diff --git a/.babelrc b/.babelrc old mode 100644 new mode 100755 index c6c9efb..57c2d42 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { -"presets": ["es2015"] +"presets": ["env"], +"plugins": ["transform-es2015-destructuring", "transform-object-rest-spread"] } \ No newline at end of file diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100755 index 0000000..78923d9 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,17 @@ +engines: + duplication: + enabled: false + config: + languages: + javascript: + mass_threshold: 20 +ratings: + paths: + - "**.js" +exclude_paths: +- "server/src/api/v1/tests/" +- "node_modules/" +- "template/" +- "server/dist/" +- "client/node_modules/" +- "server/src/docs/" diff --git a/.coveralls.yml b/.coveralls.yml index 2d8a2e0..6d10cd2 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -repo_token: COVERALLS_TOKEN \ No newline at end of file +repo_token: COVERALLS_REPO_TOKEN \ No newline at end of file diff --git a/.editorconfig b/.editorconfig old mode 100644 new mode 100755 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..6c9ac4e --- /dev/null +++ b/.env.sample @@ -0,0 +1,24 @@ +COVERALLS_REPO_TOKEN= + +TEST_DB_NAME= +TEST_DB_HOST= +TEST_DB_PASSWORD= +TEST_DB_USERNAME= + +PRODUCTION_DB_NAME= +PRODUCTION_DB_HOST= +PRODUCTION_DB_PASSWORD= +PRODUCTION_DB_USERNAME= + +DEVELOPMENT_DB_NAME= +DEVELOPMENT_DB_HOST= +DEVELOPMENT_DB_PASSWORD= +DEVELOPMENT_DB_USERNAME= + +TOKEN_SECRET= + +GMAIL_USERNAME= +GMAIL_PASSWORD= + +EMAIL_FROM= +TIMEZONE= \ No newline at end of file diff --git a/.eslintignore b/.eslintignore old mode 100644 new mode 100755 index 650380a..8b1f3ea --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,10 @@ -node_modules/ -client/ template/ -template/assets/js/** +template/assets/** server/dist/** .idea/ +template/** +coverage/ +.nyc_output/ +node_modules +.env +server/src/docs \ No newline at end of file diff --git a/.eslintrc b/.eslintrc old mode 100644 new mode 100755 index d7d5761..4d650cd --- a/.eslintrc +++ b/.eslintrc @@ -1,33 +1,36 @@ -{ - "root": true, - "extends": "airbnb-base", - "env": { - "node": true, - "es6": true, - "mocha": true - }, - "rules": { - "one-var": 0, - "one-var-declaration-per-line": 0, - "new-cap": 0, - "consistent-return": 0, - "no-param-reassign": 0, - "comma-dangle": 0, - "curly": ["error", "multi-line"], - "import/no-unresolved": [2, { "commonjs": true }], - "no-shadow": ["error", { "allow": ["req", "res", "err"] }], - "valid-jsdoc": ["error", { - "requireReturn": true, - "requireReturnType": true, - "requireParamDescription": false, - "requireReturnDescription": true - }], - "require-jsdoc": ["error", { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - "ClassDeclaration": true - } - }] - } -} \ No newline at end of file +{ + "root": true, + "extends": [ "airbnb-base" ], + "env": { + "node": true, + "es6": true, + "mocha": true + }, + "rules": { + "max-len": ["error", 80], + "one-var": 0, + "one-var-declaration-per-line": 0, + "new-cap": 0, + "consistent-return": 0, + "no-param-reassign": 0, + "comma-dangle": 0, + "curly": ["error", "multi-line"], + "import/no-unresolved": [2, { "commonjs": true }], + "no-shadow": ["error", { "allow": ["req", "res", "err"] }], + "valid-jsdoc": ["error", { + "requireReturn": true, + "requireReturnType": true, + "requireParamDescription": false, + "requireReturnDescription": true, + "matchDescription": ".+" + }], + "require-jsdoc": ["error", { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true, + "ArrowFunctionExpression": true + } + }] + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 0888f89..5f98d24 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ node_modules/ -server/src/config.json +**/node_modules/ .env .idea/ -server/dist/**/*.js -!server/dist/config \ No newline at end of file +client/src/assets/css/*.css +server/dist/** +coverage/ +.nyc_output/ +.DS_STORE +client/build +.vscode +reports diff --git a/.hound.yml b/.hound.yml old mode 100644 new mode 100755 index 624a9b2..9ad3511 --- a/.hound.yml +++ b/.hound.yml @@ -1,4 +1,4 @@ -eslint: - enabled: true - config_file: .eslintrc +eslint: + enabled: true + config_file: .eslintrc ignore_file: .eslintignore \ No newline at end of file diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000..1b327f9 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,2 @@ +instrumentation: + root: src \ No newline at end of file diff --git a/.sequelizerc b/.sequelizerc old mode 100644 new mode 100755 index 08c2859..8b9a5bf --- a/.sequelizerc +++ b/.sequelizerc @@ -1,9 +1,9 @@ const path = require('path'); - +const version = 'v1'; module.exports = { - "config": path.resolve('./server/dist/config', 'config.json'), - "models-path": path.resolve('./server/src/models'), - "seeders-path": path.resolve('./server/src/seeders'), - "migrations-path": path.resolve('./server/src/migrations') + "config": path.resolve('./server/dist/config', 'index.js'), + "models-path": path.resolve(`./server/dist/api/${version}/models`), + "seeders-path": path.resolve(`./server/dist/api/${version}/seeders`), + "migrations-path": path.resolve(`./server/dist/api/${version}/migrations`) }; diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 index aa9877a..47feb54 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,35 @@ +env: + global: + - CC_TEST_REPORTER_ID=$CODECLIMATE_REPO_TOKEN + - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi) language: node_js +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter +script: + - bundle exec rspec + # Preferably you will run test-reporter on branch update events. But + # if you setup travis to build PR updates only, you don't need to run + # the line below + - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi + # In the case where travis is setup to build PR updates only, + # uncomment the line below + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT node_js: - node notifications: - - email: false + email: false + webhooks: https://coveralls.io/webhook?repo_token=COVERALLS_REPO_TOKEN services: - postgresql before_script: - - npm install -g codeclimate-test-reporter - - npm install -g sequelize + - npm install -g codeclimate-test-reporter coveralls sequelize jest istanbul-combine - psql -c 'drop database if exists travis;' -U postgres - psql -c 'create database travis;' -U postgres - npm run build - - NODE_ENV=test npm run test:db script: - - npm test + - npm run test:coverage after_success: - - CODECLIMATE_REPO_TOKEN=CODECLIMATE_TOKEN codeclimate-test-reporter < coverage/lcov.info \ No newline at end of file + - istanbul-combine -d merged-coverage -p summary -r lcov client/coverage/coverage-*.json coverage/coverage-*.json + - codeclimate-test-reporter < merged-coverage/lcov.info + - coveralls < merged-coverage/lcov.info diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..435d689 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2017 Fadojutimi Temitayo Olusegun + +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. \ No newline at end of file diff --git a/Procfile b/Procfile old mode 100644 new mode 100755 index e8f79ea..3835a2e --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -web: npm start \ No newline at end of file +web: npm run start +clock: node server/dist/api/v1/cron/index.js \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 8513b03..08cb0d3 --- a/README.md +++ b/README.md @@ -1,63 +1,125 @@ -# HELLO-BOOKS [![Build Status](https://travis-ci.org/adesege/HelloBooks.svg?branch=development)](https://travis-ci.org/adesege/HelloBooks) [![Coverage Status](https://coveralls.io/repos/github/adesege/HelloBooks/badge.svg?branch=development)](https://coveralls.io/github/adesege/HelloBooks?branch=development) [![Code Climate](https://codeclimate.com/github/adesege/HelloBooks/badges/gpa.svg)](https://codeclimate.com/github/adesege/HelloBooks) +[![Build Status](https://travis-ci.org/adesege/HelloBooks.svg?branch=chore/153550278/feedback-implementation)](https://travis-ci.org/adesege/HelloBooks) +[![Code Climate](https://codeclimate.com/github/adesege/HelloBooks/badges/gpa.svg)](https://codeclimate.com/github/adesege/HelloBooks) +[![Test Coverage](https://codeclimate.com/github/adesege/HelloBooks/badges/coverage.svg)](https://codeclimate.com/github/adesege/HelloBooks/coverage) +[![Coverage Status](https://coveralls.io/repos/github/adesege/HelloBooks/badge.svg?branch=chore/153550278/feedback-implementation)](https://coveralls.io/github/adesege/HelloBooks?branch=chore/153550278/feedback-implementation) +[![Issue Count](https://codeclimate.com/github/adesege/HelloBooks/badges/issue_count.svg)](https://codeclimate.com/github/adesege/HelloBooks) +# Hello-Books A simple application that helps manage a library and its processes like stocking, tracking and renting books. -Built on `Javascript` with `Postgres` as database. +Built on `Nodejs`. -## HOW TO INSTALL -### PRE-REQUISITES +screen shot 2017-12-13 at 5 29 15 pm + + +## How to install +### Pre-requisites You will need to have the following installed in your working environment before this application can work. * Latest version of Nodejs - comes with a Node Package Manager * Postgresql -### INSTALLING +### Installing 1. Download or clone this branch at https://github.com/adesege/HelloBooks.git 2. Install dependencies by running `npm install`. Ensure you are in your working directory. Run `cd /path/to/HelloBooks` to change. -3. Start the server by running `npm run watch`. - -The application listens on port `8080` by default unless otherwise started in the environment port. - -Visit `http://localhost:8000` to access the front end or `http://localhost:8000/api` to access the `api` endpoint. - -## ENDPOINTS -The endpoints are listed below. -Where `:versionNumber` is the API version number. -`/api/:versionNumber` -1. `/users` - * *POST* /signup - Creates a user - * *POST* /signin - Logs a user in and generates a `JSON Web Token` - * *POST* /:userId/books - Allow users borrow a book. - * *PUT* /:userId/books - Allow users modfy a book. - * *GET* /:userId/books?returned=false - Allow users get all the books they have borrowed but are yet to return -2. `/books` - * *POST* / - Allow users add a book - * *PUT* /:bookId - Allow users modify a book information - * *GET* / - Allow users get all the books they have borrowed but are yet to return - 2. `/books/stocks` - * *POST* / - Allow admin add a stock - * *DELETE* - Allow admin delete a book - * *GET* / - Allow admin get stock - -## JSON WEB TOKEN -This application uses JSON web token to sign and verify user token. The default expiration time is `24 hours` but this can be modified in the application config. - -## MIDDLEWARES +1. Go to client directory and run `npm run install` to install dependencies. +3. Go to the root dir `cd ../` +1. Then type `npm run transpile` to transpile from es6 to es5 +1. and then run `npm run start:dev` in your terminal to start the server. +1. To start the client, `cd client` +1. Then `npm run start` + +The server and client listens on port `5000` and `3000` by default respectively unless otherwise stated as an environment variable. + +Visit `http://localhost:3000` to access the front end and `http://localhost:5000/api` to access the `api` endpoint. + +## Authentication Mechanism +This application uses JSON web token to sign and verify users. The default expiration time is `24 hours` but this can be modified in the application config. + +## Middlewares Some endpoints are restricted to logged users and admins only. E.g. Only admin can access `api/:versionNumber/books/stocks`. -There are three middlewares defined in this application. -* Authenticate middleware - verifies a user's token and checks if the user is valid. -* userAuthenticate middleware - verifies if the user is `user` -* adminAuthenticate middleware - checks if the user is `admin`. +There are two middlewares defined in this application. +* authMiddleware - verifies a user's token and checks if the user is valid. +* adminMiddleware - checks if the user is `admin`. + +## Documentation +> If Ruby is already installed, but the bundle command doesn't work, just run `gem install bundler` in a terminal. +### Starting the server +1. `cd slate` from the root directory. +1. Initialize and start Slate. You can either do this locally, or with Vagrant: +```js + npm run slate:install + # then + npm run slate:build + # finally + npm run slate:start + # OR run this to run with vagrant + vagrant up +``` + +Please visit the application documentation at http://hellobooks.herokuapp.com/docs/v1 or http://localhost:4567 to test locally + +To deploy generated files into your application, run `npm run slate:build` + +# Testing +There are three different kind of testing in this application; client, server and end-to-end testing. + +> To start the server test. In your CMD, run +```js + npm run test + + # To get coverage result, run + npm run test:coverage +``` + +> To start the end-to-end test +```js +# For the first time you are running it, +# you'll need to install the selenium server and chrome driver. +# To do that, run +npm run test:e2e-install + +# then start the server with +npm run test:e2e-server + +# finally, run the test with +npm run test:e2e +``` + +> Ensure that the server and client are up and running. The client must listen to port 3000. Otherwise, change it in [nightwatch.conf:16](./nightwatch.json#L16) + +> Finally, for the client test, +``` +# first cd to the test folder +# then run +npm run test + +# for coverage report, run +npm run test:coverage +``` + +# Author + +**Temitayo Fadojutimi** is a Software Developer at Andela and he dedicates his expertise to solving practical problems in the society. He tweets at [@adesege_](http://twitter.com/adesege_) -## RESPONSE STATUSES -These are the common status codes used in the app. +# Contributing Guide -1. `400 Bad Request` - Used when there is a validation error. -2. `500 Internal Server Error` - A generic error message, given when no more specific message is suitable. -3. `404 Not Found` - Used when the application returns an empty result. -4. `200 OK` - All is well and set. -5. `401 Unauthorized` - Used when authentication failed. -6. `403 Forbidden` - Used when a user is accessing a restricted end point. -7. `201 Created` - Used when a new record is inserted into the database. +Thank you for your interest in contributing to this package. I currently accept contributions from everyone but I expect that standards are maintained. +To contribute, +1. Fork the project +1. Create a feature branch, branch away from `master` +1. Write tests, using `Mocha and Chai` or any other testing frameworks, and code +1. If you have multiple commits please combine them into a few logically organized commits by squashing them. +1. Push the commit(s) to your fork +1. Submit a merge request (MR) to the `master` branch +1. The MR title should describe the change you want to make +1. The MR description should give a motive for your change and the method you used to achieve it. + 1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or + `Closes #XXX` syntax to auto-close the issue(s) once the merge request will + be merged. +1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission + 1. If a discussion has been addressed, select the "Resolve discussion" button beneath it to mark it resolved. +1. When writing commit messages please follow + [these guidelines](http://chris.beams.io/posts/git-commit). -## CONTRIBUTING -All contributions are welcome. Just create a push request, mention me and I will have a look at it. \ No newline at end of file +# License +This project is licensed under the MIT license. Click **[here](./LICENSE.md)** to read the license in full \ No newline at end of file diff --git a/client/.babelrc b/client/.babelrc new file mode 100755 index 0000000..586b56f --- /dev/null +++ b/client/.babelrc @@ -0,0 +1,25 @@ +{ + "presets": ["env", "react"], + "plugins": [ + "transform-object-rest-spread", + "transform-es2015-destructuring", + [ + "module-resolver", + { + "root": ["./src"], + "alias":{ + "form": ["./src/components/form"], + "Modal": ["./src/components/Modal"] + } + } + ] + ], + "env": { + "production": { + "plugins": [ + "transform-react-remove-prop-types", + "transform-react-inline-elements" + ] + } + } +} \ No newline at end of file diff --git a/client/.env.sample b/client/.env.sample new file mode 100644 index 0000000..729127c --- /dev/null +++ b/client/.env.sample @@ -0,0 +1,23 @@ +PORT= +NODE_ENV= +APP_CLOUD_NAME= +APP_API_KEY= +APP_API_SECRET= +APP_CLOUDINARY_URL= + + +AUTH_GOOGLE_CLIENT_ID= +AUTH_GOOGLE_CLIENT_SECRET= +AUTH_GOOGLE_CALLBACK= + +AUTH_FACEBOOK_CLIENT_ID= +AUTH_FACEBOOK_CLIENT_SECRET= +AUTH_FACEBOOK_CALLBACK= + +DISQUS_SHORT_NAME= +ROOT_URL= + +TIMEZONE= +SOCKET_URL= + +API_VERSION= \ No newline at end of file diff --git a/client/.eslintignore b/client/.eslintignore new file mode 100755 index 0000000..c6ada23 --- /dev/null +++ b/client/.eslintignore @@ -0,0 +1,7 @@ +.idea/ +assets/** +public/** +coverage +node_modules +.env +__tests__ \ No newline at end of file diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100755 index 0000000..d750550 --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,50 @@ +{ + "root": true, + "extends": [ "airbnb-base", "react" ], + "env": { + "node": true, + "es6": true, + "browser": true, + "jest": true + }, + "settings": { + "import/resolver": { + "webpack": { + "config": "./config/webpack.dev.js" + }, + "node": { + "extensions": [".js",".jsx", ".spec.js", ".spec.jsx"] + } + } + }, + "rules": { + "indent": ["error", 2], + "prefer-reflect": 0, + "max-len": [1, 80, 2], + "one-var": 0, + "one-var-declaration-per-line": 0, + "new-cap": 0, + "consistent-return": 0, + "no-param-reassign": 0, + "comma-dangle": 0, + "curly": ["error", "multi-line"], + "import/no-unresolved": [2, { "commonjs": true }], + "no-shadow": ["error", { "allow": ["req", "res", "err"] }], + "valid-jsdoc": ["error", { + "requireReturn": true, + "requireReturnType": true, + "requireParamDescription": false, + "requireReturnDescription": true, + "matchDescription": ".+" + }], + "require-jsdoc": ["error", { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true, + "ArrowFunctionExpression": true, + "FunctionExpression": true + } + }] + } + } \ No newline at end of file diff --git a/client/.hound.yml b/client/.hound.yml new file mode 100755 index 0000000..9ad3511 --- /dev/null +++ b/client/.hound.yml @@ -0,0 +1,4 @@ +eslint: + enabled: true + config_file: .eslintrc + ignore_file: .eslintignore \ No newline at end of file diff --git a/client/__tests__/__mocks__/actions/book.js b/client/__tests__/__mocks__/actions/book.js new file mode 100644 index 0000000..2c9caa7 --- /dev/null +++ b/client/__tests__/__mocks__/actions/book.js @@ -0,0 +1,41 @@ +export const bookData = { + title: 'A new title', + coverPhotoPath: '', + documentPath: '' +}; + +export const response = { + message: ['Book added successfully'], + book: { + title: 'A new book title', + id: 1 + }, + id: 1 +}; + +export const searchResponse = { + books: { + ...response.book + } +}; + +export const getBookResponse = { + books: { ...response.book }, + pagination: { + pageSize: 1, + totalCount: 10, + page: 0, + pageCount: 1, + limit: 10 + }, + message: ['There was an unexpected error'] +}; + +export const responseFailure = { + message: ['The title field is required'] +}; + +export const signinResponseFailure = { + message: ['Sorry, we can\'t find this account'] +}; + diff --git a/client/__tests__/__mocks__/actions/borrowedBooks.js b/client/__tests__/__mocks__/actions/borrowedBooks.js new file mode 100644 index 0000000..da88343 --- /dev/null +++ b/client/__tests__/__mocks__/actions/borrowedBooks.js @@ -0,0 +1,33 @@ +const book = { + userId: 1, + bookId: 1 +}; + +export default { + book, + response: { + message: 'You have successfully borrowed this book', + id: 1, + ...book + }, + error: { + message: ['There are no more copies left of this book to borrow'] + }, + getRequestData: { + userId: 1 + }, + getResponseData: { + books: { + ...book, + createdAt: '2017-01-01' + } + }, + returnRequestData: { + userId: 1, + bookId: 1, + borrowedBookId: 1 + }, + returnResponseData: { + message: 'You have successfully returned this book' + } +}; diff --git a/client/__tests__/__mocks__/actions/user.js b/client/__tests__/__mocks__/actions/user.js new file mode 100644 index 0000000..e8bce03 --- /dev/null +++ b/client/__tests__/__mocks__/actions/user.js @@ -0,0 +1,17 @@ + +export const response = { + message: ['Your account has been created successfully'], + user: { + group: 'user', + userId: 1, + } +}; + +export const signupResponseFailure = { + message: ['The name field is required'] +}; + +export const signinResponseFailure = { + message: ['Sorry, we can\'t find this account'] +}; + diff --git a/client/__tests__/__mocks__/file.js b/client/__tests__/__mocks__/file.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/client/__tests__/__mocks__/file.js @@ -0,0 +1 @@ +export default {}; diff --git a/client/__tests__/__mocks__/localStorage.js b/client/__tests__/__mocks__/localStorage.js new file mode 100644 index 0000000..de031d8 --- /dev/null +++ b/client/__tests__/__mocks__/localStorage.js @@ -0,0 +1,46 @@ + +/** + * @class localStorage + */ +class localStorage { + /** + * @static + * @param {any} key + * @param {any} value + * @returns {object} setItem + * @memberOf localStorage + */ + static setItem(key, value) { + return Object.assign(localStorage, { [key]: value }); + } + + /** + * @static + * @param {any} key + * @returns {object} item + * @memberOf localStorage + */ + static getItem(key) { + return localStorage[key]; + } + + /** + * @static + * @param {any} key + * @returns {undefined} + * @memberOf localStorage + */ + static removeItem(key) { + delete localStorage[key]; + } + + /** + * @static + * @returns {undefined} + * @memberOf localStorage + */ + static clear() { + localStorage = {}; + } +} +export default localStorage; diff --git a/client/__tests__/__mocks__/style.js b/client/__tests__/__mocks__/style.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/client/__tests__/__mocks__/style.js @@ -0,0 +1 @@ +export default {}; diff --git a/client/__tests__/__mocks__/worker.js b/client/__tests__/__mocks__/worker.js new file mode 100644 index 0000000..727ede2 --- /dev/null +++ b/client/__tests__/__mocks__/worker.js @@ -0,0 +1 @@ +export default Object.create(null); diff --git a/client/__tests__/actions/auth.spec.js b/client/__tests__/actions/auth.spec.js new file mode 100644 index 0000000..3086761 --- /dev/null +++ b/client/__tests__/actions/auth.spec.js @@ -0,0 +1,312 @@ +import moxios from 'moxios'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import expect from 'expect'; + +import { + login, + logout, + sendResetPasswordMail, + resetPassword +} from 'actions/auth'; +import { + userSignupRequestAction +} from 'actions/signupActions'; +import { + response, + signupResponseFailure, + signinResponseFailure +} from '../__mocks__/actions/user'; + +/* eslint-disable max-nested-callbacks */ + +const mockStore = configureMockStore([ + thunk +]); + +describe('# Auth', () => { + beforeEach(() => moxios.install()); + afterEach(() => moxios.uninstall()); + + describe('# Signup', () => { + it( + 'creates SET_CURRENT_USER when signup action is successful', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: ['Your account has been created successfully'], + type: 'success' + } + }, + { type: 'SET_CURRENT_USER', user: response.user } + ]; + + const store = mockStore({ }); + return store.dispatch(userSignupRequestAction({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + + it( + 'should not create a user when signup action fails', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: signupResponseFailure, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: ['The name field is required'], + type: 'error' + } + } + ]; + + const store = mockStore({}); + return store.dispatch(userSignupRequestAction({})).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('# Signin', () => { + it( + 'should create SET_CURRENT_USER when signin action is successful', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response, + }); + }); + + const expectedActions = [ + { type: 'SET_CURRENT_USER', user: response.user } + ]; + + const store = mockStore({ }); + return store.dispatch(login({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + + it( + 'should not log a user in when signin action fails', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: signinResponseFailure, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: ['Sorry, we can\'t find this account'], + type: 'error' + }, + } + ]; + + const store = mockStore({ }); + return store.dispatch(login({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('Logout', () => { + it( + 'should log a user out when the logout action is dispatched', + (done) => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response, + }); + }); + + const expectedActions = [ + { type: 'SET_CURRENT_USER', user: {} } + ]; + + const store = mockStore({ }); + store.dispatch(logout()); + expect(store.getActions()).toEqual(expectedActions); + done(); + } + ); + }); + + describe('Send Reset password mail', () => { + it( + 'should dispatch success flash message '+ + 'action when password reset mail has been sent', + (done) => { + const message = [ + 'A password reset link has been sent to test@hellobooks.com. ' + + 'It may take upto 5 mins for the mail to arrive.' + ]; + moxios.stubRequest('users/reset-password', { + status: 200, + response: { + message + } + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(sendResetPasswordMail({})) + .then(() => { + const receivedOption = store.getActions(); + expect(receivedOption).toEqual(expectedActions); + done(); + }) + .catch((error) => done(error)); + } + ); + + it( + 'should dispatch error flash message ' + + 'action when password reset mail cannot be sent', + (done) => { + const message = [ + 'There was an error processing your request' + ]; + moxios.stubRequest('users/reset-password', { + status: 400, + response: { + message + } + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(sendResetPasswordMail({})) + .then(() => { + const receivedOption = store.getActions(); + expect(receivedOption).toEqual(expectedActions); + done(); + }) + .catch((error) => done(error)); + } + ); + }); + describe('Reset password', () => { + it( + 'should dispatch success flash message'+ + ' action when password has been changed', + (done) => { + const message = [ + 'Password successfully changed. Please login to your account.' + ]; + moxios.stubRequest('users/reset-password/verify', { + status: 200, + response: { + message + } + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(resetPassword({})) + .then(() => { + const receivedOption = store.getActions(); + expect(receivedOption).toEqual(expectedActions); + done(); + }) + .catch((error) => done(error)); + } + ); + + it( + 'should dispatch error flash message ' + + 'action when password cannot be changed', + (done) => { + const message = [ + 'There was an error processing your request' + ]; + moxios.stubRequest('users/reset-password/verify', { + status: 400, + response: { + message + } + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(resetPassword({})) + .then(() => { + const receivedOption = store.getActions(); + expect(receivedOption).toEqual(expectedActions); + done(); + }) + .catch((error) => done(error)); + } + ); + }); +}); diff --git a/client/__tests__/actions/books.spec.js b/client/__tests__/actions/books.spec.js new file mode 100644 index 0000000..dde195e --- /dev/null +++ b/client/__tests__/actions/books.spec.js @@ -0,0 +1,268 @@ +import moxios from 'moxios'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import expect from 'expect'; + +import { + addBook, + updateBook, + deleteBook, + searchBooks, + getBooks +} from 'actions/books'; +import localStorage from '../__mocks__/localStorage'; +import { + bookData, + response, + searchResponse, + getBookResponse +} from '../__mocks__/actions/book'; + +/* eslint-disable max-nested-callbacks */ + +const mockStore = configureMockStore([ + thunk +]); + +window.localStorage = localStorage; + +describe('# Books', () => { + beforeEach(() => moxios.install()); + afterEach(() => moxios.uninstall()); + + describe('# Add a book', () => { + it('should create BOOK_ADDED when a book has been added', () => { + moxios.stubRequest('books', { + status: 200, + response + }); + + moxios + .stubRequest( + 'books/1?fields[]=coverPhotoPath&fields[]=documentPath', + { + status: 200, + response + } + ); + const expectedActions = [ + { + type: 'BOOK_ADDED', + book: response.book + } + ]; + + const store = mockStore({ }); + return store.dispatch(addBook(bookData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + }); + + describe('# Edit a book', () => { + it('should create BOOK_UPDATED action when a book has been edited', + () => { + const newResponse = { + ...response, + message: ['Book updated successfully'] + }; + + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: newResponse, + }); + }); + + moxios + .stubRequest('books/1?fields[]=coverPhotoPath&fields[]=documentPath', + { + status: 200, + response: newResponse + } + ); + + const expectedActions = [ + { + type: 'BOOK_UPDATED', + book: response.book + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: ['Book updated successfully'], + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(updateBook(bookData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + }); + + describe('# Delete a book', () => { + it('should create BOOK_DELETED action when a book has been deleted', + () => { + const bookId = 1; + const newResponse = { + ...response, + message: ['Book deleted successfully'], + bookId: 1 + }; + + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: newResponse, + }); + }); + + const expectedActions = [ + { + type: 'BOOK_DELETED', + bookId + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: newResponse.message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(deleteBook({ id: bookId })) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it( + 'should not create BOOK_DELETED action when a book can\'t be deleted', + () => { + const newResponse = { + ...response, + message: ['Book not found'], + bookId: 1 + }; + + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: newResponse, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: newResponse.message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(deleteBook({ id: 1 })) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('# Search a book', () => { + it( + 'should create BOOKS_SEARCHED action when a book has been searched', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: searchResponse, + }); + }); + + const expectedActions = [ + { + type: 'BOOKS_SEARCHED', + result: response.book, + } + ]; + + const store = mockStore({ }); + return store.dispatch(searchBooks(bookData.title)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('# Get a book', () => { + it('should create SET_BOOKS action when a book has been gotten', () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { + books: getBookResponse.books, + pagination: getBookResponse.pagination + }, + }); + }); + + const expectedActions = [ + { + type: 'SET_BOOKS', + books: getBookResponse.books, + pagination: getBookResponse.pagination, + } + ]; + + const store = mockStore({ }); + return store.dispatch(getBooks(bookData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + it( + 'should not create SET_BOOKS action when a book can\'t be gotten', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 500, + response: getBookResponse, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: getBookResponse.message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(getBooks(bookData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); +}); diff --git a/client/__tests__/actions/borrowedBooks.spec.js b/client/__tests__/actions/borrowedBooks.spec.js new file mode 100644 index 0000000..3ed28bc --- /dev/null +++ b/client/__tests__/actions/borrowedBooks.spec.js @@ -0,0 +1,210 @@ +import moxios from 'moxios'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import expect from 'expect'; + +import { + borrowBook, + getBorrowedBooks, + returnBorrowedBook +} from 'actions/borrowedBooks'; +import borrowedBookMock from '../__mocks__/actions/borrowedBooks'; + +/* eslint-disable max-nested-callbacks */ + +const mockStore = configureMockStore([ + thunk +]); + +describe('# Borrowed Books', () => { + beforeEach(() => moxios.install()); + afterEach(() => moxios.uninstall()); + + describe('Borrow a book', () => { + it( + 'should create BOOK_BORROWED and '+ + 'ADD_FLASH_MESSAGE when a book has been borrowed', + () => { + moxios.stubRequest('users/1/books', { + status: 200, + response: borrowedBookMock.response + }); + + const expectedActions = [ + { + type: 'BOOK_BORROWED', + book: borrowedBookMock.book, + isReturned: false + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: borrowedBookMock.response.message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(borrowBook(borrowedBookMock.book)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + it('should create BOOK_BORROWED when a book has been borrowed', () => { + moxios.stubRequest('users/1/books', { + status: 400, + response: borrowedBookMock.error + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: borrowedBookMock.error.message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(borrowBook(borrowedBookMock.book)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + }); + + describe('Get Borrowed book', () => { + it( + 'should create GET_BORROWED_BOOKS when request is successful', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: borrowedBookMock.getResponseData, + }); + }); + + const expectedActions = [ + { + type: 'GET_BORROWED_BOOKS', + book: borrowedBookMock.getResponseData.books + } + ]; + + const store = mockStore({ }); + return store + .dispatch(getBorrowedBooks(borrowedBookMock.getRequestData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + + it( + 'should create ADD_FLASH_MESSAGE when request is not successful', + () => { + const message = ['There was an error processing your request']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: { + message + }, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store + .dispatch(getBorrowedBooks(borrowedBookMock.getRequestData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('Returned Borrowed book', () => { + it( + 'should create BOOK_RETURNED when request is successful', + () => { + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: borrowedBookMock.returnResponseData, + }); + }); + + const expectedActions = [ + { + type: 'BOOK_RETURNED', + book: borrowedBookMock.returnRequestData, + isReturned: true + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: borrowedBookMock.returnResponseData.message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store + .dispatch(returnBorrowedBook(borrowedBookMock.returnRequestData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + + it( + 'should create error ADD_FLASH_MESSAGE when request is not successful', + () => { + const message = ['There was an error completing your request']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: { + message + }, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store + .dispatch(returnBorrowedBook(borrowedBookMock.returnRequestData)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); +}); diff --git a/client/__tests__/actions/categories.spec.js b/client/__tests__/actions/categories.spec.js new file mode 100644 index 0000000..447cfba --- /dev/null +++ b/client/__tests__/actions/categories.spec.js @@ -0,0 +1,192 @@ +import moxios from 'moxios'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import expect from 'expect'; + +import { + addBookCategory, + getBookCategories, + editBookCategory +} from 'actions/categories'; + +/* eslint-disable max-nested-callbacks */ + +const mockStore = configureMockStore([ + thunk +]); + +const category = { + id: 1, + name: 'Category1' +}; + +describe('# Categories', () => { + beforeEach(() => moxios.install()); + afterEach(() => moxios.uninstall()); + + describe('# Add a book category', () => { + it('should create CATEGORY_ADDED when a category has been added', + () => { + const message = ['Category added successfully']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { + category, + message + }, + }); + }); + + const expectedActions = [ + { + type: 'CATEGORY_ADDED', + category + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'success' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(addBookCategory({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it( + 'should create ADD_FLASH_MESSAGE when a category cannot be added', + () => { + const message = ['There was an error processing your request']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: { + message + }, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'error' + } + } + ]; + + const store = mockStore({ }); + return store.dispatch(addBookCategory({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); + + describe('# Get book category', () => { + it( + 'should create CATEGORY_FETCHED when a category has been fetched', + () => { + const message = ['Category added successfully']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { + categories: category, + message + }, + }); + }); + + const expectedActions = [ + { + type: 'CATEGORY_FETCHED', + categories: category + } + ]; + const store = mockStore({ }); + return store.dispatch(getBookCategories({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + + it('should create ADD_FLASH_MESSAGE when a category can\'t ', () => { + const message = ['Category added successfully']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 400, + response: { + message + }, + }); + }); + + const expectedActions = [ + { + type: 'ADD_FLASH_MESSAGE', + message: { + type: 'error', + text: message + } + } + ]; + const store = mockStore({ }); + return store.dispatch(getBookCategories({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + }); + + describe('# Edit book category', () => { + it( + 'should create CATEGORY_EDITED when a category has been edited', + () => { + const message = ['Category edited successfully']; + moxios.wait(() => { + const request = moxios.requests.mostRecent(); + request.respondWith({ + status: 200, + response: { + category, + message + }, + }); + }); + + const expectedActions = [ + { + type: 'CATEGORY_EDITED', + category + }, + { + type: 'ADD_FLASH_MESSAGE', + message: { + text: message, + type: 'success' + } + } + ]; + const store = mockStore({ }); + return store.dispatch(editBookCategory({})) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + } + ); + }); +}); diff --git a/client/__tests__/assets/js/modal.spec.js b/client/__tests__/assets/js/modal.spec.js new file mode 100644 index 0000000..da380cb --- /dev/null +++ b/client/__tests__/assets/js/modal.spec.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { onClickOpenBookCover } from 'assets/js/modal'; + +describe('# onClickOpenBookCover', () => { + it('should call onClickOpenBookCover', () => { + expect(onClickOpenBookCover(global.event, '#target')) + .toBeUndefined(); + }); +}); diff --git a/client/__tests__/assets/js/removeBookBg.spec.js b/client/__tests__/assets/js/removeBookBg.spec.js new file mode 100644 index 0000000..70c9db2 --- /dev/null +++ b/client/__tests__/assets/js/removeBookBg.spec.js @@ -0,0 +1,8 @@ +import React from 'react'; +import removeBookBg from 'assets/js/removeBookBg'; + +describe('# removeBookBg', () => { + it('should call removeBookBg', () => { + expect(removeBookBg()).toBeUndefined(); + }); +}); diff --git a/client/__tests__/assets/js/socket/index.spec.js b/client/__tests__/assets/js/socket/index.spec.js new file mode 100644 index 0000000..0415608 --- /dev/null +++ b/client/__tests__/assets/js/socket/index.spec.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { + ioGetNotifications, + ioJoin, + ioNewNotifications } from 'assets/js/socket/'; + +describe('# Socket', () => { + it('should call ioGetNotifications', () => { + expect(ioGetNotifications()).toBeUndefined(); + }); + + it('should call ioJoin', () => { + expect(ioJoin()).toBeUndefined(); + }); + + it('should call ioNewNotifications', () => { + expect(ioNewNotifications()).toBeUndefined(); + }); +}); diff --git a/client/__tests__/components/App.spec.js b/client/__tests__/components/App.spec.js new file mode 100644 index 0000000..1a44660 --- /dev/null +++ b/client/__tests__/components/App.spec.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import App from 'components/App'; + +jest.mock('react-router'); + +const props = { + children:
+}; + +describe('# App', () => { + const wrapper = () => shallow(); + it('should render App component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper().find('ErrorBoundary').length).toBe(1); + expect(wrapper().find('.loadingBar').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/BooksList/SearchFilter.spec.js b/client/__tests__/components/Books/BooksList/SearchFilter.spec.js new file mode 100644 index 0000000..81dce9d --- /dev/null +++ b/client/__tests__/components/Books/BooksList/SearchFilter.spec.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import SearchFilter from 'components/Books/BooksList/SearchFilter'; + +const props = { + onChangeInput: jest.fn(), + onSearchFilter: jest.fn(), + categories: [{ + id: 1, + name: 'name' + }], + searchFilter: {} +}; + +describe('# SearchFilter', () => { + const wrapper = shallow(); + it('should render SearchFilter component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('form'); + }); +}); diff --git a/client/__tests__/components/Books/BooksList/index.spec.js b/client/__tests__/components/Books/BooksList/index.spec.js new file mode 100644 index 0000000..682e8d6 --- /dev/null +++ b/client/__tests__/components/Books/BooksList/index.spec.js @@ -0,0 +1,114 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedBooksList, { BooksList } from 'components/Books/BooksList'; + +const categories = [{ + id: 1, + name: 'Category 1' +}]; + +const props = { + books: [], + userGroup: '', + goToEditPage: jest.fn(), + confirmDelete: jest.fn(), + pagination: {}, + handlePageChange: jest.fn(), + getBooks: jest.fn(), + getBookCategories: jest.fn(), + categories: [], +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +const state = { + books: { + books: [], + pagination: {} + }, + auth: { + user: { + group: '' + } + }, + categories: [] +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + ...state +}); + +describe('# BooksList', () => { + const wrapper = shallow(, context); + it('should render BooksList component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest.spyOn( + wrapper.instance(), + 'componentWillReceiveProps' + ); + const nextProps = { + params: { id: 2 }, + books: '', + categories, + pagination: {} + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + it('should call the goToEditPage method', () => { + const goToEditPageOnSpy = jest.spyOn(wrapper.instance(), 'goToEditPage'); + wrapper.instance().goToEditPage(global.event); + expect(goToEditPageOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the confirmDelete method', () => { + const confirmDeleteOnSpy = jest.spyOn(wrapper.instance(), 'confirmDelete'); + wrapper.instance().confirmDelete(global.event); + expect(confirmDeleteOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the handlePageChange method', () => { + const handlePageChangeOnSpy = jest + .spyOn(wrapper.instance(), 'handlePageChange'); + wrapper.instance().handlePageChange(global.event); + expect(handlePageChangeOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the onSearchFilter method', () => { + const onSearchFilterOnSpy = jest + .spyOn(wrapper.instance(), 'onSearchFilter'); + wrapper.instance().onSearchFilter(global.event); + expect(onSearchFilterOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChangeInput method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeInputOnSpy = jest.spyOn(wrapper.instance(), 'onChangeInput'); + wrapper.instance().onChangeInput(newEvent); + expect(onChangeInputOnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/__tests__/components/Books/BooksModal.spec.js b/client/__tests__/components/Books/BooksModal.spec.js new file mode 100644 index 0000000..fe6da67 --- /dev/null +++ b/client/__tests__/components/Books/BooksModal.spec.js @@ -0,0 +1,141 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedBooksModal, { + BooksModal + } from 'components/Books/BooksModal'; +import Modal from 'Modal'; + +const book = { + id: 2, + title: 'A book title', + coverPhotoPath: '', + bookCategoryId: 0, + author: '', + stockQuantity: 0, + ISBN: '', + publishedDate: '', + description: '' +}; + +const categories = [{ + id: 1, + name: 'Category 1' +}]; + +const props = { + params: { + id: 2 + }, + book, + coverPhotoPath: '', + addFlashMessage: jest.fn(), + addBook: jest.fn(), + updateBook: jest.fn(), + getBook: jest.fn(), + setBooks: jest.fn(), + getCategoriesAction: jest.fn(), + categories +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + books: book, + cropper: { coverPhotoPath: '' }, + categories +}); + +describe('# BooksModal', () => { + const wrapper = shallow(, context); + wrapper.setState({ book }); + it('should render BooksModal component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe(Modal); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + params: { id: 2 }, + book, + categories + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + it('should call the goToBooksPage method', () => { + const goToBooksPageOnSpy = jest + .spyOn(wrapper.instance(), 'goToBooksPage'); + wrapper.instance().goToBooksPage(); + expect(goToBooksPageOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the toggleOpenModal method', () => { + const toggleOpenModalOnSpy = jest + .spyOn(wrapper.instance(), 'toggleOpenModal'); + wrapper.instance().toggleOpenModal(); + expect(toggleOpenModalOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the closeOnClick method', () => { + const closeOnClickOnSpy = jest.spyOn(wrapper.instance(), 'closeOnClick'); + wrapper.instance().closeOnClick(global.event); + expect(closeOnClickOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the isFormValid method', () => { + const isFormValidOnSpy = jest.spyOn(wrapper.instance(), 'isFormValid'); + wrapper.instance().isFormValid(); + expect(isFormValidOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + + it('should call the onClickOk method', () => { + const onClickOkOnSpy = jest.spyOn(wrapper.instance(), 'onClickOk'); + wrapper.instance().onClickOk(); + expect(onClickOkOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + describe('onChangeUploadInput', () => { + it('should call the onChangeUploadInput method', () => { + const onChangeUploadInputOnSpy = jest + .spyOn(wrapper.instance(), 'onChangeUploadInput'); + wrapper.instance().onChangeUploadInput(global.event); + expect(onChangeUploadInputOnSpy).toHaveBeenCalledTimes(1); + }); + it('should return false if window.FileReader cannot be found', () => { + window.FileReader = false; + const onChangeUploadInputOnSpy = jest + .spyOn(wrapper.instance(), 'onChangeUploadInput'); + wrapper.instance().onChangeUploadInput(global.event); + expect(onChangeUploadInputOnSpy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/client/__tests__/components/Books/Categories/index.spec.js b/client/__tests__/components/Books/Categories/index.spec.js new file mode 100644 index 0000000..2f97b80 --- /dev/null +++ b/client/__tests__/components/Books/Categories/index.spec.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedCategories, { Categories } from 'components/Books/Categories'; + +const props = { + addFlashMessage: jest.fn(), + addBookCategoryAction: jest.fn(), + getBookCategoriesAction: jest.fn(), + editBookCategoryAction: jest.fn(), + deleteBookCategoryAction: jest.fn(), + bookCategories: [] +}; + +const bookCategories = [{ + id: 1, + name: 'Category name' +}]; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({}); + +describe('# Categories', () => { + const wrapper = shallow(); + + it('should render Categories component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSubmit method', () => { + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onDeleteSubmit method', () => { + const onDeleteSubmitOnSpy = jest + .spyOn(wrapper.instance(), 'onDeleteSubmit'); + wrapper.instance().onDeleteSubmit(global.event); + expect(onDeleteSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onDeleteModal method', () => { + const onDeleteModalOnSpy = jest + .spyOn(wrapper.instance(), 'onDeleteModal'); + wrapper.instance().onDeleteModal(global.event); + expect(onDeleteModalOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + bookCategories, + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onEditClick method', () => { + const onEditClickOnSpy = jest.spyOn(wrapper.instance(), 'onEditClick'); + wrapper.instance().onEditClick(global.event); + expect(onEditClickOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + }); + \ No newline at end of file diff --git a/client/__tests__/components/Books/DeleteModal.spec.js b/client/__tests__/components/Books/DeleteModal.spec.js new file mode 100644 index 0000000..9036228 --- /dev/null +++ b/client/__tests__/components/Books/DeleteModal.spec.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedDeleteModal, { + DeleteModal + } from 'components/Books/DeleteModal'; + +const book = { + id: 2, + title: 'A book title', + coverPhotoPath: '', + bookCategoryId: 0, + author: '', + stockQuantity: 0, + ISBN: '', + publishedDate: '', + description: '' +}; + +const props = { + params: { + id: 2 + }, + book, + deleteBook: jest.fn(() => Promise.resolve()) +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + books: { books: [book] }, +}); + +describe('# DeleteModal', () => { + const wrapper = shallow(, context); + it('should call the goToBooksPage method', () => { + const goToBooksPageOnSpy = jest + .spyOn(wrapper.instance(), 'goToBooksPage'); + wrapper.instance().goToBooksPage(); + expect(goToBooksPageOnSpy).toHaveBeenCalledTimes(1); + }); + it('should render DeleteModal component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + + it('should call the toggleOpenModal method', () => { + const toggleOpenModalOnSpy = jest + .spyOn(wrapper.instance(), 'toggleOpenModal'); + wrapper.instance().toggleOpenModal(); + expect(toggleOpenModalOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onClickOk method', () => { + const onClickOkOnSpy = jest.spyOn(wrapper.instance(), 'onClickOk'); + wrapper.instance().onClickOk(global.event); + expect(onClickOkOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/__tests__/components/Books/Histories/Filter.spec.js b/client/__tests__/components/Books/Histories/Filter.spec.js new file mode 100644 index 0000000..c12226e --- /dev/null +++ b/client/__tests__/components/Books/Histories/Filter.spec.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Filter from 'components/Books/Histories/Filter'; + +const props = { + onChangeInput: jest.fn(), + onSearchFilter: jest.fn(), + searchFilter: {} +}; + +describe('# Filter', () => { + const wrapper = shallow(); + it('should render Filter component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('form').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/Histories/List.spec.js b/client/__tests__/components/Books/Histories/List.spec.js new file mode 100644 index 0000000..9604f37 --- /dev/null +++ b/client/__tests__/components/Books/Histories/List.spec.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedList, { List } from 'components/Books/Histories/List'; + +const histories = [{ + id: 1, + Book: { + id: 1, + title: 'Hello world', + coverPhotoPath: '' + } +}]; + +const props = { + histories, + userId: 1, + getHistoriesAction: jest.fn(), + isReturned: true, + pagination: {} +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + }, + histories: { + histories: [{ + id: 1 + }], + pagination: { } + } +}); + +describe('# List', () => { + const wrapper = shallow(); + wrapper.setState({ histories }); + it('should render List component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + histories: [{ + ...histories[0], + id: 2 + }], + pagination: { limit: 10 } + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the handlePageChange method', () => { + const handlePageChangeSpy = jest + .spyOn(wrapper.instance(), 'handlePageChange'); + wrapper.instance().handlePageChange(1); + expect(handlePageChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSearchFilter method', () => { + const onSearchFilterSpy = jest + .spyOn(wrapper.instance(), 'onSearchFilter'); + wrapper.instance().onSearchFilter(global.event); + expect(onSearchFilterSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChangeInput method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeInputOnSpy = jest.spyOn(wrapper.instance(), 'onChangeInput'); + wrapper.instance().onChangeInput(newEvent); + expect(onChangeInputOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/Histories/index.spec.js b/client/__tests__/components/Books/Histories/index.spec.js new file mode 100644 index 0000000..29684d3 --- /dev/null +++ b/client/__tests__/components/Books/Histories/index.spec.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Histories from 'components/Books/Histories'; + +const props = { + title: '', + config: {} +}; + +describe('# Histories', () => { + const wrapper = shallow(); + it('should render Histories component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.title').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/StockManager/SearchStock.spec.js b/client/__tests__/components/Books/StockManager/SearchStock.spec.js new file mode 100644 index 0000000..53b2138 --- /dev/null +++ b/client/__tests__/components/Books/StockManager/SearchStock.spec.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedSearchStock, { + SearchStock +} from 'components/Books/StockManager/SearchStock'; + +const props = { + searchBooksAction: jest.fn(), + addFlashMessage: jest.fn(), + searchResult: [] +}; + +const searchResult = [{ + id: 1, + title: 'Book title' +}]; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + books: { + books: [] + } +}); + +describe('# SearchStock', () => { + const wrapper = shallow(, context); + + it('should render SearchStock component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSubmit method', () => { + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSelect method', () => { + const onSelectOnSpy = jest.spyOn(wrapper.instance(), 'onSelect'); + wrapper.instance().onSelect('0000-00-00', 2); + expect(onSelectOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + searchResult, + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(, context); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/StockManager/ShowStock/index.spec.js b/client/__tests__/components/Books/StockManager/ShowStock/index.spec.js new file mode 100644 index 0000000..2c55750 --- /dev/null +++ b/client/__tests__/components/Books/StockManager/ShowStock/index.spec.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedShowStock, +{ + ShowStock +} from 'components/Books/StockManager/ShowStock'; + +const props = { + params: { + id: 2 + }, + stocks: [{ + book: { + title: 'title' + } + }], + getStockByBookIdAction: jest.fn(), + addStockAction: jest.fn(() => Promise.resolve()), + deleteStockAction: jest.fn(() => Promise.resolve()), +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + stocks: {} +}); + +describe('# ShowStock', () => { + const wrapper = shallow(, context); + it('should render ShowStock component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + stocks: [{ + book: { + title: 'title' + } + }] + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + it('should call the toggleOpenAddModal method', () => { + const toggleOpenAddModalOnSpy = jest + .spyOn(wrapper.instance(), 'toggleOpenAddModal'); + wrapper.instance().toggleOpenAddModal(); + expect(toggleOpenAddModalOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the onDeleteModal method', () => { + const onDeleteModalOnSpy = jest + .spyOn(wrapper.instance(), 'onDeleteModal'); + wrapper.instance().onDeleteModal(global.event); + expect(onDeleteModalOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the onSubmit method', () => { + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the onDeleteSubmit method', () => { + const onDeleteSubmitOnSpy = jest + .spyOn(wrapper.instance(), 'onDeleteSubmit'); + wrapper.instance().onDeleteSubmit(global.event); + expect(onDeleteSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/__tests__/components/Books/UploadBookCover.spec.js b/client/__tests__/components/Books/UploadBookCover.spec.js new file mode 100644 index 0000000..31669ee --- /dev/null +++ b/client/__tests__/components/Books/UploadBookCover.spec.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import CropperJs from 'react-cropperjs'; +import TestUtils from 'react-dom/test-utils'; +import ConnectedUploadBookCover, { + UploadBookCover + } from 'components/Books/UploadBookCover'; + +const book = { + id: 2, + title: 'A book title', + coverPhotoPath: '', + bookCategoryId: 0, + author: '', + stockQuantity: 0, + ISBN: '', + publishedDate: '', + description: '' +}; + +const props = { + params: { + id: 2 + }, + book, + deleteImageData: jest.fn(), + setImageData: jest.fn() +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + books: { books: [book] }, +}); + +describe('# UploadBookCover', () => { + const wrapper = shallow(, context); + it('should render UploadBookCover component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe(CropperJs); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); + + it('should call the crop method', () => { + const spy = jest.fn(); + const cropRefs = TestUtils + .renderIntoDocument(); + cropRefs.refs.cropper.getCroppedCanvas = jest.fn(() => ({ + toDataURL: jest.fn() + })); + cropRefs.crop(); + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/client/__tests__/components/Books/ViewBooks/BookComment.spec.js b/client/__tests__/components/Books/ViewBooks/BookComment.spec.js new file mode 100644 index 0000000..2e9857a --- /dev/null +++ b/client/__tests__/components/Books/ViewBooks/BookComment.spec.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import BookComment from 'components/Books/ViewBooks/BookComment'; + +const props = { + title: '', + config: {} +}; + +describe('# BookComment', () => { + const wrapper = shallow(); + it('should render BookComment component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('div').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/ViewBooks/BorrowBook.spec.js b/client/__tests__/components/Books/ViewBooks/BorrowBook.spec.js new file mode 100644 index 0000000..bf7ac05 --- /dev/null +++ b/client/__tests__/components/Books/ViewBooks/BorrowBook.spec.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import BorrowBook from 'components/Books/ViewBooks/BorrowBook'; + +const props = { + bookId: '', + isBorrowedBook: false, + userId: 0, + borrowBookAction: jest.fn(() => Promise.resolve()), + returnBorrowedBookAction: jest.fn(() => Promise.resolve()), + borrowedBook: { + id: 1, + createdAt: '0000-00-00' + } +}; + +describe('# BorrowBook', () => { + const wrapper = shallow(); + it('should render BorrowBook component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('ButtonDropdown').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/ViewBooks/index.spec.js b/client/__tests__/components/Books/ViewBooks/index.spec.js new file mode 100644 index 0000000..d7ae32c --- /dev/null +++ b/client/__tests__/components/Books/ViewBooks/index.spec.js @@ -0,0 +1,135 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedViewBooks, { ViewBooks } from 'components/Books/ViewBooks'; + +const book = { + id: 1, + title: 'A book title', + coverPhotoPath: '', + bookCategoryId: 0, + author: '', + stockQuantity: 0, + ISBN: '', + publishedDate: '', + description: '', + Category: { + name: '' + } +}; + +const borrowedBook = { + id: 1, + bookId: 1 +}; + +const props = { + book, + userId: 1, + borrowedBook, + borrowBookAction: jest.fn(() => Promise.resolve()), + getBorrowedBooksAction: jest.fn(() => Promise.resolve()), + returnBorrowedBookAction: jest.fn(() => Promise.resolve()), + params: { + id: 1 + }, + getBooks: jest.fn(() => Promise.resolve()) +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + }, + books: { + books: [{ + id: 1, + title: 'A book title' + }] + }, + borrowedBooks: [{ + ...borrowedBook + }] +}); + +describe('# ViewBooks', () => { + const wrapper = shallow(, context); + wrapper.setState({ book }); + it('should render ViewBooks component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + params: { id: 1 }, + book: { + ...book, + title: 'A title' + }, + borrowedBook: { + ...borrowedBook, + createdAt: '2017-11-11' + } + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it( + 'should call the toggleDropdown method', + () => { + const toggleDropdownOnSpy = jest + .spyOn(wrapper.instance(), 'toggleDropdown'); + wrapper.instance().toggleDropdown(); + expect(toggleDropdownOnSpy).toHaveBeenCalledTimes(1); + } + ); + + it('should call the onReturnBorrowedBook method', () => { + const onReturnBorrowedBookOnSpy = jest + .spyOn(wrapper.instance(), 'onReturnBorrowedBook'); + wrapper.instance().onReturnBorrowedBook(global.event); + expect(onReturnBorrowedBookOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onBorrowBook method', () => { + const onBorrowBookOnSpy = jest.spyOn(wrapper.instance(), 'onBorrowBook'); + wrapper.instance().onBorrowBook(global.event); + expect(onBorrowBookOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Books/index.spec.js b/client/__tests__/components/Books/index.spec.js new file mode 100644 index 0000000..91c793a --- /dev/null +++ b/client/__tests__/components/Books/index.spec.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Books from 'components/Books'; + +const book = { + id: 2, + title: 'A book title', + coverPhotoPath: '', + bookCategoryId: 0, + author: '', + stockQuantity: 0, + ISBN: '', + publishedDate: '', + description: '' +}; + +const props = { + params: { + id: 2 + }, + book, + deleteBook: jest.fn(() => Promise.resolve()) +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + + +describe('# Books', () => { + const wrapper = shallow(, context); + it('should render Books component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); +}); diff --git a/client/__tests__/components/Dashboard/index.spec.js b/client/__tests__/components/Dashboard/index.spec.js new file mode 100644 index 0000000..601d75c --- /dev/null +++ b/client/__tests__/components/Dashboard/index.spec.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Dashboard from 'components/Dashboard'; + +const props = { + message: { + text: [], + type: 'error' + } +}; + +describe('# Dashboard', () => { + const wrapper = shallow(); + it('should render Dashboard component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.title').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/FlashMessagesList/FlashMessage.spec.js b/client/__tests__/components/FlashMessagesList/FlashMessage.spec.js new file mode 100644 index 0000000..9b1ce35 --- /dev/null +++ b/client/__tests__/components/FlashMessagesList/FlashMessage.spec.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FlashMessage from 'components/FlashMessagesList/FlashMessage'; + +const props = { + message: { + text: [], + type: 'error' + } +}; + +describe('# FlashMessage', () => { + const wrapper = shallow(); + it('should render FlashMessage component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('div').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/FlashMessagesList/index.spec.js b/client/__tests__/components/FlashMessagesList/index.spec.js new file mode 100644 index 0000000..42da4be --- /dev/null +++ b/client/__tests__/components/FlashMessagesList/index.spec.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedFlashMessagesList, { + FlashMessagesList +} from 'components/FlashMessagesList'; + +const notifications = [{ + id: 1, + Book: { + id: 1, + title: 'Hello world', + coverPhotoPath: '' + } +}]; + +const props = { + message: {}, + deleteFlashMessageAction: jest.fn() +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + flashMessages: {} +}); + +describe('# Notifications', () => { + const wrapper = shallow(); + wrapper.setState({ notifications }); + it('should render Notifications component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('SetTimeout').length).toBe(1); + }); + + it('should call the componentWillUnmount method', () => { + const componentWillUnmountSpy = jest + .spyOn(wrapper.instance(), 'componentWillUnmount'); + wrapper.instance().componentWillUnmount(); + expect(componentWillUnmountSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Modal/Body.spec.js b/client/__tests__/components/Modal/Body.spec.js new file mode 100644 index 0000000..96a1263 --- /dev/null +++ b/client/__tests__/components/Modal/Body.spec.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Body from 'components/Modal/Body'; + +const props = { + children: {} +}; + +describe('# Body component', () => { + const wrapper = shallow(); + + it('should render Body component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('ModalBody').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Modal/Header.spec.js b/client/__tests__/components/Modal/Header.spec.js new file mode 100644 index 0000000..123e201 --- /dev/null +++ b/client/__tests__/components/Modal/Header.spec.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Header from 'components/Modal/Header'; + +const props = { + title: '', + headerOptions: [

] +}; + +describe('# Header component', () => { + const wrapper = shallow(
); + + it('should render Header component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); +}); diff --git a/client/__tests__/components/Notifications/List.spec.js b/client/__tests__/components/Notifications/List.spec.js new file mode 100644 index 0000000..4b0cf9c --- /dev/null +++ b/client/__tests__/components/Notifications/List.spec.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import List from 'components/Notifications/List'; + +const props = { + notifications: [{ + id: 1, + title: '', + updatedAt: '', + notificationType: '', + Book: { + author: '' + }, + User: { + name: '' + } + }], + pagination: {}, + handlePageChange: jest.fn(), + isPagination: true +}; + +describe('# List', () => { + const wrapper = shallow(); + it('should render Notification List component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.title').length).toBe(1); + }); + + it('should render Empty message component', () => { + wrapper.setProps({ notifications: [] }); + expect(wrapper).toBeDefined(); + expect(wrapper.find('EmptyMessage').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Notifications/NotificationType.spec.js b/client/__tests__/components/Notifications/NotificationType.spec.js new file mode 100644 index 0000000..3bc311b --- /dev/null +++ b/client/__tests__/components/Notifications/NotificationType.spec.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import NotificationType from 'components/Notifications/NotificationType'; + +const props = { + type: 'BOOK_BORROWED' +}; + +describe('# NotificationType', () => { + const wrapper = shallow(); + it('should render BOOK_BORROWED button', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.text-info').length).toBe(1); + }); + + it('should render BOOK_RETURNED button', () => { + wrapper.setProps({ type: 'BOOK_RETURNED' }); + expect(wrapper).toBeDefined(); + expect(wrapper.find('.text-success').length).toBe(1); + }); + + it('should render BOOK_SURCHARGED button', () => { + wrapper.setProps({ type: 'BOOK_SURCHARGED' }); + expect(wrapper).toBeDefined(); + expect(wrapper.find('.btn-warning').length).toBe(1); + }); + + it('should not render notification type button', () => { + wrapper.setProps({ type: '' }); + expect(wrapper).toBeDefined(); + expect(wrapper.find('div').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Notifications/Sidebar.spec.js b/client/__tests__/components/Notifications/Sidebar.spec.js new file mode 100644 index 0000000..63555b2 --- /dev/null +++ b/client/__tests__/components/Notifications/Sidebar.spec.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Sidebar from 'components/Notifications/Sidebar'; + +const props = { + searchFilter: {}, + handleInputChange: jest.fn(), + onFilterSubmit: jest.fn(), + errors: {}, +}; + +describe('# Sidebar', () => { + const wrapper = shallow(); + it('should render Sidebar component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.card-header').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Notifications/index.spec.js b/client/__tests__/components/Notifications/index.spec.js new file mode 100644 index 0000000..8b95cb8 --- /dev/null +++ b/client/__tests__/components/Notifications/index.spec.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedNotifications, { + Notifications +} from 'components//Notifications'; + +const notifications = [{ + id: 1, + Book: { + id: 1, + title: 'Hello world', + coverPhotoPath: '' + } +}]; + +const props = { + notifications, + getNotifications: jest.fn(), + pagination: {} +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + }, + notifications: { + notifications: [{ + id: 1 + }], + pagination: { } + } +}); + +describe('# Notifications', () => { + const wrapper = shallow(); + wrapper.setState({ notifications }); + it('should render Notifications component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + notifications: [{ + ...notifications[0], + id: 2 + }], + pagination: { limit: 10 } + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the handlePageChange method', () => { + const handlePageChangeSpy = jest + .spyOn(wrapper.instance(), 'handlePageChange'); + wrapper.instance().handlePageChange(1); + expect(handlePageChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onFilterSubmit method', () => { + const onFilterSubmitSpy = jest + .spyOn(wrapper.instance(), 'onFilterSubmit'); + wrapper.instance().onFilterSubmit(global.event); + expect(onFilterSubmitSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the handleInputChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const handleInputChangeOnSpy = jest + .spyOn(wrapper.instance(), 'handleInputChange'); + wrapper.instance().handleInputChange(newEvent); + expect(handleInputChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Profile/ChangePassword.spec.js b/client/__tests__/components/Profile/ChangePassword.spec.js new file mode 100644 index 0000000..a83c850 --- /dev/null +++ b/client/__tests__/components/Profile/ChangePassword.spec.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ChangePasswordForm from 'components/Profile/ChangePasswordForm'; + +const props = { + onChangePasswordInput: jest.fn(), + onChangePassword: jest.fn(), + isOpenModal: true, + toggleOpenModal: jest.fn(), + isLoading: true, + errors: {}, + serverErrors: {} +}; + +describe('# ChangePasswordForm', () => { + const wrapper = shallow(); + it('should render ChangePasswordForm component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('Modal').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Profile/Info.spec.jsx b/client/__tests__/components/Profile/Info.spec.jsx new file mode 100644 index 0000000..b90025d --- /dev/null +++ b/client/__tests__/components/Profile/Info.spec.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Info from 'components/Profile/Info'; + +const props = { + user: { + name: 'test', + email: 'test@mail.com' + }, + onChangePasswordInput: jest.fn(), + onChangePassword: jest.fn(), + isOpenModal: true, + toggleOpenModal: jest.fn(), + isLoading: true, + errors: {}, + serverErrors: {} +}; + +describe('# Info', () => { + const wrapper = shallow(); + it('should render Info component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.row').length).toBe(2); + }); +}); diff --git a/client/__tests__/components/Profile/index.spec.js b/client/__tests__/components/Profile/index.spec.js new file mode 100644 index 0000000..65fc305 --- /dev/null +++ b/client/__tests__/components/Profile/index.spec.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedProfile, { Profile } from 'components//Profile'; + +const users = [{ + id: 1, + name: 'Test' +}]; + +const props = { + userId: 1, + getUsersAction: jest.fn(), + addFlashMessage: jest.fn(), + updateUserAction: jest.fn(() => Promise.resolve({ + response: { + status: 200 + } + })), + userGroup: 'user', + user: {} +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + }, + users +}); + +describe('# Profile', () => { + const wrapper = shallow(); + wrapper.setState({ + user: users, + passwordChange: { + oldPassword: 'password', + password: 'password', + confirmPassword: 'password', + userId: 1 + } + }); + it('should render Profile component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the componentWillReceiveProps method', () => { + const componentWillReceivePropsSpy = jest + .spyOn(wrapper.instance(), 'componentWillReceiveProps'); + const nextProps = { + user: users[0], + pagination: { limit: 10 } + }; + wrapper.instance().componentWillReceiveProps(nextProps); + expect(componentWillReceivePropsSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChangePassword method', () => { + const onChangePasswordSpy = jest + .spyOn(wrapper.instance(), 'onChangePassword'); + wrapper.instance().onChangePassword(global.event); + expect(onChangePasswordSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the toggleOpenModal method', () => { + const toggleOpenModalSpy = jest + .spyOn(wrapper.instance(), 'toggleOpenModal'); + wrapper.instance().toggleOpenModal(); + expect(toggleOpenModalSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChangePasswordInput method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangePasswordInputOnSpy = jest + .spyOn(wrapper.instance(), 'onChangePasswordInput'); + wrapper.instance().onChangePasswordInput(newEvent); + expect(onChangePasswordInputOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the isFormValid method', () => { + const isFormValidSpy = jest + .spyOn(wrapper.instance(), 'isFormValid'); + wrapper.instance().isFormValid(); + expect(isFormValidSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/Routes/index.spec.js b/client/__tests__/components/Routes/index.spec.js new file mode 100644 index 0000000..043d2ab --- /dev/null +++ b/client/__tests__/components/Routes/index.spec.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Routes from '../../../src/Routes'; + +jest.mock('react-router'); + +describe('# Routes', () => { + it('should render application routes', () => { + const wrapper = shallow(); + expect(wrapper).toBeDefined(); + }); +}); diff --git a/client/__tests__/components/homepage/Login/index.spec.js b/client/__tests__/components/homepage/Login/index.spec.js new file mode 100644 index 0000000..9018297 --- /dev/null +++ b/client/__tests__/components/homepage/Login/index.spec.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedLogin, { Login } from 'components/homepage/Login'; + +const props = { + login: jest.fn(), + addFlashMessage: jest.fn(), + setCurrentUser: jest.fn(), + logUserIn: jest.fn(), + isAuthenticated: true +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +const user = { + email: '', + password: '', + oauthID: '' +}; + +const errorResponse = { + error: 'popup_closed_by_user' +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + } +}); + +describe('# Login', () => { + const wrapper = shallow(, context); + it('should render Login component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onFacebookCallback method', () => { + const onFacebookCallbackOnSpy = jest + .spyOn(wrapper.instance(), 'onFacebookCallback'); + wrapper.instance().onFacebookCallback(user); + expect(onFacebookCallbackOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onGoogleCallback method', () => { + const onGoogleCallbackOnSpy = jest + .spyOn(wrapper.instance(), 'onGoogleCallback'); + wrapper.instance().onGoogleCallback({ profileObj: user }); + expect(onGoogleCallbackOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onGoogleFailure method', () => { + const onGoogleFailureOnSpy = jest + .spyOn(wrapper.instance(), 'onGoogleFailure'); + wrapper.instance().onGoogleFailure(errorResponse); + expect(onGoogleFailureOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the isFormValid method', () => { + const isFormValidOnSpy = jest + .spyOn(wrapper.instance(), 'isFormValid'); + wrapper.instance().isFormValid(); + expect(isFormValidOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/homepage/ResetPassword/ChangePassword/ChangePasswordForm.spec.js b/client/__tests__/components/homepage/ResetPassword/ChangePassword/ChangePasswordForm.spec.js new file mode 100644 index 0000000..e021c91 --- /dev/null +++ b/client/__tests__/components/homepage/ResetPassword/ChangePassword/ChangePasswordForm.spec.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ChangePasswordForm from +'components/homepage/ResetPassword/ChangePassword/ChangePasswordForm'; + +const props = { + user: {}, + isLoading: false, + onChange: jest.fn(), + onSubmit: jest.fn(), + errors: {}, +}; + +describe('# Change Password Form', () => { + const wrapper = shallow(); + + it('should render ChangePasswordForm component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('form'); + }); +}); diff --git a/client/__tests__/components/homepage/ResetPassword/ChangePassword/index.spec.js b/client/__tests__/components/homepage/ResetPassword/ChangePassword/index.spec.js new file mode 100644 index 0000000..bca8c02 --- /dev/null +++ b/client/__tests__/components/homepage/ResetPassword/ChangePassword/index.spec.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedChangePassword, { + ChangePassword +} from 'components/homepage/ResetPassword/ChangePassword'; + +const props = { + addFlashMessage: jest.fn(), + resetPasswordAction: jest.fn(), + params: { + validationKey: '2345678sdfghjkl' + } +}; + +const user = { + email: '', + password: '', + confirmPassword: '', + validationKey: '' +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({}); + +describe('# ChangePassword', () => { + const wrapper = shallow(, context); + + it('should render ChangePassword component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSubmit method', () => { + wrapper.setState({ user }); + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the isFormValid method', () => { + wrapper.setState({ user }); + const isFormValidOnSpy = jest.spyOn(wrapper.instance(), 'isFormValid'); + wrapper.instance().isFormValid(); + expect(isFormValidOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(, context); + expect(connectedComponent.length).toBe(1); + }); + }); + \ No newline at end of file diff --git a/client/__tests__/components/homepage/ResetPassword/ResetPasswordForm.spec.js b/client/__tests__/components/homepage/ResetPassword/ResetPasswordForm.spec.js new file mode 100644 index 0000000..f2429e3 --- /dev/null +++ b/client/__tests__/components/homepage/ResetPassword/ResetPasswordForm.spec.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ResetPasswordForm from + 'components/homepage/ResetPassword/ResetPasswordForm'; + +const props = { + user: {}, + isLoading: true, + onChange: jest.fn(), + onSubmit: jest.fn(), + errors: { + email: 'This field is required' + } +}; + +describe('# ResetPasswordForm', () => { + const wrapper = shallow(); + + it('should render ResetPasswordForm component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('form'); + }); +}); diff --git a/client/__tests__/components/homepage/ResetPassword/index.spec.js b/client/__tests__/components/homepage/ResetPassword/index.spec.js new file mode 100644 index 0000000..b9991ca --- /dev/null +++ b/client/__tests__/components/homepage/ResetPassword/index.spec.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedResetPassword, { + ResetPassword + } from 'components/homepage/ResetPassword'; + +const props = { + addFlashMessage: jest.fn(), + resetPasswordAction: jest.fn(() => Promise.resolve()), + params: { + validationKey: '2345678sdfghjkl' + } +}; + +const user = { + email: '' +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({}); + +describe('# ResetPassword', () => { + const wrapper = shallow(); + + it('should render ResetPassword component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSubmit method', () => { + wrapper.setState({ + user: { + email: 'hello@books.com', + password: 'aaaa', + confirmPassword: 'aaaa' + } + }); + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the isFormValid method', () => { + wrapper.setState({ user }); + const isFormValidOnSpy = jest.spyOn(wrapper.instance(), 'isFormValid'); + wrapper.instance().isFormValid(); + expect(isFormValidOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(); + expect(connectedComponent.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/homepage/Signup/SignupForm.spec.js b/client/__tests__/components/homepage/Signup/SignupForm.spec.js new file mode 100644 index 0000000..a7dea89 --- /dev/null +++ b/client/__tests__/components/homepage/Signup/SignupForm.spec.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import SignupForm from 'components/homepage/Signup/SignupForm'; + +const props = { + userSignupRequest: jest.fn(), + addFlashMessage: jest.fn(), + logUserIn: jest.fn(), + isLoading: true, + onChange: jest.fn(), + onSubmit: jest.fn(), + user: {}, + validationError: { + name: 'Error', + email: 'email@email.com', + password: 'password', + confirmPassword: 'password' + } +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +describe('# SignupForm', () => { + const wrapper = shallow(, context); + it('should render SignupForm component', () => { + expect(wrapper.length).toBe(1); + }); +}); diff --git a/client/__tests__/components/homepage/Signup/index.spec.js b/client/__tests__/components/homepage/Signup/index.spec.js new file mode 100644 index 0000000..f94eae7 --- /dev/null +++ b/client/__tests__/components/homepage/Signup/index.spec.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedSignup, { Signup } from 'components/homepage/Signup'; + +const props = { + addFlashMessage: jest.fn(), + userSignupRequest: jest.fn(() => Promise.resolve()), + logUserIn: jest.fn() +}; + +const context = { + context: { + router: { + push: jest.fn() + } + } +}; + +const user = { + id: '34567897654321', + name: 'name', + email: 'email', + password: 'password', + confirmPassword: 'password', + oauthID: '1234567890' +}; + + +const mockStore = configureMockStore([thunk]); +const store = mockStore({}); + +describe('# Signup', () => { + const wrapper = shallow(, context); + wrapper.setState({ user }); + it('should call the onFacebookCallback method', () => { + const onFacebookCallbackOnSpy = jest + .spyOn(wrapper.instance(), 'onFacebookCallback'); + wrapper.instance().onFacebookCallback(user); + expect(onFacebookCallbackOnSpy).toHaveBeenCalledTimes(1); + }); + it('should call the onGoogleCallback method', () => { + const onGoogleCallbackOnSpy = jest + .spyOn(wrapper.instance(), 'onGoogleCallback'); + wrapper.instance().onGoogleCallback({ profileObj: user }); + expect(onGoogleCallbackOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onGoogleFailure method', () => { + const onGoogleFailureOnSpy = jest + .spyOn(wrapper.instance(), 'onGoogleFailure'); + wrapper.instance().onGoogleFailure(); + expect(onGoogleFailureOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render Signup component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.getElement().type).toBe('div'); + }); + + it('should call the onChange method', () => { + const newEvent = { + ...global.event, + target: { + name: 'name', + value: 'value' + } + }; + const onChangeOnSpy = jest.spyOn(wrapper.instance(), 'onChange'); + wrapper.instance().onChange(newEvent); + expect(onChangeOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call the onSubmit method', () => { + wrapper.setState({ user }); + const onSubmitOnSpy = jest.spyOn(wrapper.instance(), 'onSubmit'); + wrapper.instance().onSubmit(global.event); + expect(onSubmitOnSpy).toHaveBeenCalledTimes(1); + }); + + it('should render the connected component', () => { + const connectedComponent = shallow(, context); + expect(connectedComponent.length).toBe(1); + }); + }); + \ No newline at end of file diff --git a/client/__tests__/components/layouts/Dashboard/Menu/index.spec.js b/client/__tests__/components/layouts/Dashboard/Menu/index.spec.js new file mode 100644 index 0000000..e531a4a --- /dev/null +++ b/client/__tests__/components/layouts/Dashboard/Menu/index.spec.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import Menu from 'components/layouts/Dashboard/Menu'; +import configureMockStore from 'redux-mock-store'; + + +jest.mock('react-router'); + +const props = { + auth: {}, + navigationLinks: {}, + logout: jest.fn() +}; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({ + auth: { + user: { + userId: 1 + } + } +}); +let wrapper; + +describe('# Menu Layout', () => { + wrapper = shallow( + , { + context: { + router: { + push: jest.fn() + } + } + }); + it('should render Menu component', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('Navbar').length).toBe(1); + }); +}); diff --git a/client/__tests__/components/layouts/Dashboard/index.spec.js b/client/__tests__/components/layouts/Dashboard/index.spec.js new file mode 100644 index 0000000..f4e4908 --- /dev/null +++ b/client/__tests__/components/layouts/Dashboard/index.spec.js @@ -0,0 +1,127 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Content from 'components/layouts/Dashboard/Content'; +import Footer from 'components/layouts/Dashboard/Footer'; +import Header from 'components/layouts/Dashboard/Header'; +import Menu from 'components/layouts/Dashboard/Menu'; +import NotificationsList from 'components/layouts/Dashboard/NotificationsList'; +import NavigationLinks from 'components/layouts/Dashboard/NavigationLinks'; +import Dashboard from 'components/layouts/Dashboard'; +import EmptyMessage from 'components/miscellaneous/EmptyMessage'; +import FlashMessagesList from 'components/FlashMessagesList'; + +const props = { + children:
, + isAuthenticated: true, + user: {}, + logout: jest.fn(), + group: '', + auth: {}, + navigationLinks: {}, + notifications: [{ + User: { + name: 'User\'s name' + }, + Book: { + title: 'Book title' + }, + notificationType: 'BOOK_BORROWED' + }], + isPagination: true, + isOpen: true, + isDropdownOpen: false, + isNotificationDropdownOpen: false, + isNewNotification: false, + toggleDropdown: jest.fn(), + toggleNotificationDropdown: jest.fn(), + menuNotifications: [] + +}; + +describe('# Dashboard Layout', () => { + it('should render Content component', () => { + const contentWrapper = shallow(); + expect(contentWrapper).toBeDefined(); + expect(contentWrapper.find(FlashMessagesList).length).toBe(1); + expect(contentWrapper.find('#contentArea').length).toBe(1); + }); + + it('should render Footer component', () => { + const footerWrapper = shallow(