diff --git a/.coveralls.yml b/.coveralls.yml index 6d10cd2..43a6a2c 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -repo_token: COVERALLS_REPO_TOKEN \ No newline at end of file +repo_token: RF9Gn0DcrIITCfsdfz4hrfRUC2kUGq0qF \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index ed147b2..85a2547 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,2 @@ /server/docs -/server/test /dist -/client/__tests__ diff --git a/.travis.yml b/.travis.yml index 4700364..484edf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,13 @@ language: node_js node_js: - - "6" + - "8" env: global: - export NODE_ENV=test +before_script: + - npm install -g codeclimate-test-reporter script: - npm test after_success: + - codeclimate-test-reporter < coverage/lcov.info - npm run coverage diff --git a/README.md b/README.md index 7d6bb95..c35e71c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ [![License](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/segunolalive/helloBooks.svg?branch=development)](https://travis-ci.org/segunolalive/helloBooks) -[![Coverage Status](https://coveralls.io/repos/github/segunolalive/helloBooks/badge.svg?branch=development)](https://coveralls.io/github/segunolalive/helloBooks?branch=development) +[![Test Coverage](https://codeclimate.com/github/segunolalive/helloBooks/badges/coverage.svg)](https://codeclimate.com/github/segunolalive/helloBooks/coverage) +[![Coverage Status](https://coveralls.io/repos/github/segunolalive/helloBooks/badge.svg?branch=development)](https://coveralls.io/github/segunolalive/helloBooks) [![Code Climate](https://codeclimate.com/github/segunolalive/helloBooks/badges/gpa.svg)](https://codeclimate.com/github/segunolalive/helloBooks?branch=development) ## A Library app @@ -63,7 +64,7 @@ npm run start #### API Documentation -* +* #### Testing @@ -79,7 +80,7 @@ Then start the client by running `npm run start:client`. In another terminal window, run `npm run start:server:e2e` to start the application server in test-mode. -In a third terminal window run `npm run e2e:server` to start the selenium server. +In a third terminal window run `npm run e2e-server` to start the selenium server. In a fourth terminal window run `npm run test:e2e` diff --git a/client/__tests__/__mocks__/Notify.js b/client/__tests__/__mocks__/Notify.js new file mode 100644 index 0000000..189621b --- /dev/null +++ b/client/__tests__/__mocks__/Notify.js @@ -0,0 +1,6 @@ +import Notify from '../../actions/Notify'; + +Notify.success = jest.spyOn(Notify, 'success'); +Notify.error = jest.spyOn(Notify, 'error'); + +export default Notify; diff --git a/client/__tests__/__mocks__/mockLocalStorage.js b/client/__tests__/__mocks__/mockLocalStorage.js index 1cd02dc..6c2314f 100644 --- a/client/__tests__/__mocks__/mockLocalStorage.js +++ b/client/__tests__/__mocks__/mockLocalStorage.js @@ -1,17 +1,17 @@ -const localStorage = { +const mockLocalStorage = { store: {}, setItem(key, value) { - return ({ ...localStorage.store, [key]: value }); + return ({ ...mockLocalStorage.store, [key]: value }); }, getItem(key) { - return localStorage.store[key]; + return mockLocalStorage.store[key]; }, removeItem(key) { - delete localStorage.store[key]; + delete mockLocalStorage.store[key]; }, clear() { - localStorage.store = {}; + mockLocalStorage.store = {}; } }; -export default localStorage; +export default mockLocalStorage; diff --git a/client/__tests__/__mocks__/notify.js b/client/__tests__/__mocks__/notify.js deleted file mode 100644 index 7f292ff..0000000 --- a/client/__tests__/__mocks__/notify.js +++ /dev/null @@ -1,7 +0,0 @@ -import notify from '../../actions/notify'; - -jest.unmock('../../actions/notify'); -notify.success = jest.fn(); -notify.error = jest.fn(); - -export default notify; diff --git a/client/__tests__/__mocks__/superagent.js b/client/__tests__/__mocks__/superagent.js new file mode 100644 index 0000000..8bfc59d --- /dev/null +++ b/client/__tests__/__mocks__/superagent.js @@ -0,0 +1,45 @@ +let mockDelay; +let mockError; +let mockResponse = { + status() { + return 200; + }, + ok: true, + get: jest.genMockFunction(), + toError: jest.genMockFunction(), + data: { message: 'success' } +}; + +const request = { + post: jest.genMockFunction().mockReturnThis(), + get: jest.genMockFunction().mockReturnThis(), + send: jest.genMockFunction().mockReturnThis(), + query: jest.genMockFunction().mockReturnThis(), + field: jest.genMockFunction().mockReturnThis(), + set: jest.genMockFunction().mockReturnThis(), + accept: jest.genMockFunction().mockReturnThis(), + timeout: jest.genMockFunction().mockReturnThis(), + end: jest.genMockFunction().mockImplementation(function cb(callback) { + if (mockDelay) { + this.delayTimer = setTimeout(callback, 0, mockError, mockResponse); + return; + } + + callback(mockError, mockResponse); + }), + + __setMockDelay(boolValue) { + mockDelay = boolValue; + }, + __setMockResponse(mockRes) { + mockResponse = mockRes; + }, + __setMockError(mockErr) { + mockError = mockErr; + }, + __setMockResponseBody(body) { + mockResponse.body = body; + } +}; + +export default request; diff --git a/client/__tests__/actions/adminActions.spec.js b/client/__tests__/actions/adminActions.spec.js index b261c3b..b7dadf8 100644 --- a/client/__tests__/actions/adminActions.spec.js +++ b/client/__tests__/actions/adminActions.spec.js @@ -10,9 +10,12 @@ import { addBook, } from '../../actions/adminActions/books'; import { fetchNotifications } from '../../actions/adminActions/notifications'; import actionTypes from '../../actions/actionTypes'; +import uploadFile from '../../actions/uploadFile'; +import Notify from '../__mocks__/Notify'; -import notify from '../__mocks__/notify'; +window.CLOUDINARY_API_BASE = 'CLOUDINARY_API_BASE'; +window.CLOUDINARY_UPLOAD_PRESET = 'CLOUDINARY_UPLOAD_PRESET'; const middleware = [thunk]; const mockStore = configureMockStore(middleware); @@ -22,16 +25,23 @@ describe('ADMIN ACTIONS', () => { afterEach(() => moxios.uninstall()); describe('addBook', () => { - it('returns a success toast on success', () => { + it('creates CREATE_BOOK action type and a toast on success', () => { + const book = { title: 4, authors: 'Funke' }; moxios.stubRequest('/api/v1/books', { status: 200, - response: { message: 'success' } - }); - const expectedActions = []; + response: { + message: 'success', + book + }, + }); + const expectedActions = [{ + book, + type: 'CREATE_BOOK' + }]; const store = mockStore({}); - return store.dispatch(addBook(1)).then(() => { + return store.dispatch(addBook(book)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); }); }); @@ -44,35 +54,44 @@ describe('ADMIN ACTIONS', () => { const store = mockStore({}); return store.dispatch(addBook(1)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); + expect(Notify.error.mock.calls[0]).toEqual(['failure']); }); }); }); describe('editBook', () => { - it('returns a success toast on success', () => { + it('creates EDIT_BOOK_INFO action type and a toast on success', () => { + const book = { title: 4, authors: 'Funke' }; moxios.stubRequest('/api/v1/books/1', { status: 200, - response: { message: 'success' } - }); - const expectedActions = []; + response: { + message: 'success', + book + }, + }); + const expectedActions = [{ + book, + type: 'EDIT_BOOK_INFO' + }]; const store = mockStore({}); return store.dispatch(editBook(1, {})).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); + expect(Notify.success.mock.calls[0]).toEqual(['success']); }); }); - it('returns a success toast on failure', () => { + it('returns a failure toast on failure', () => { moxios.stubRequest('/api/v1/books/1', { status: 500, - response: { message: 'success' } + response: { message: 'failure' } }); const expectedActions = []; const store = mockStore({}); return store.dispatch(editBook(1, {})).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); @@ -87,20 +106,20 @@ describe('ADMIN ACTIONS', () => { const store = mockStore({}); return store.dispatch(deleteBook(1)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); }); }); it('toasts a message and creates no action on failure', () => { moxios.stubRequest('/api/v1/books/1', { status: 500, - response: { message: 'success' } + response: { message: 'failure' } }); const expectedActions = []; const store = mockStore({}); return store.dispatch(deleteBook(1)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); @@ -109,13 +128,15 @@ describe('ADMIN ACTIONS', () => { it('returns a success toast on success', () => { moxios.stubRequest('/api/v1/books/category', { status: 200, - response: { message: 'success' } + response: { message: 'success', category: 'new category' } }); - const expectedActions = []; + const expectedActions = [ + { type: 'ADD_BOOK_CATEGORY', category: 'new category' } + ]; const store = mockStore({}); return store.dispatch(addBookCategory('category')).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); }); }); @@ -124,18 +145,20 @@ describe('ADMIN ACTIONS', () => { status: 500, response: { message: 'failure' } }); - const expectedActions = []; + const expectedActions = [{ + message: 'failure', + type: 'ADD_BOOK_CATEGORY_FAILURE', + }]; const store = mockStore({}); return store.dispatch(addBookCategory('category')).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); describe('fetchNotifications', () => { - it('it creates IS_FETCHING_NOTIFICATIONS, GET_ADMIN_NOTIFICATIONS ' + - 'and SET_NOTICATIONS_PAGINATION when successful', () => { + it('it creates IS_FETCHING_NOTIFICATIONS, GET_ADMIN_NOTIFICATIONS and SET_NOTICATIONS_PAGINATION when successful', () => { const { notifications } = mockStoreData.notificationReducer; const { pagination } = mockStoreData.notificationReducer; moxios.stubRequest('/api/v1/admin-notifications', { @@ -151,34 +174,32 @@ describe('ADMIN ACTIONS', () => { const store = mockStore({}); return store.dispatch(fetchNotifications()).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); - }); - }); - - it('it creates IS_FETCHING_NOTIFICATIONS, GET_MORE_ADMIN_NOTIFICATIONS ' + - 'and SET_NOTICATIONS_PAGINATION when successfully called with offset > 0', - () => { - const { notifications } = mockStoreData.notificationReducer; - const { pagination } = mockStoreData.notificationReducer; - moxios.stubRequest('/api/v1/admin-notifications?offset=20&', { - status: 200, - response: { message: 'success', notifications, metadata: pagination } - }); - const expectedActions = [ - { type: actionTypes.IS_FETCHING_NOTIFICATIONS, status: true }, - { type: actionTypes.SET_NOTICATIONS_PAGINATION, pagination }, - { type: actionTypes.GET_MORE_ADMIN_NOTIFICATIONS, notifications }, - { type: actionTypes.IS_FETCHING_NOTIFICATIONS, status: false } - ]; - const store = mockStore({}); - return store.dispatch(fetchNotifications({ offset: 20 })).then(() => { - expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); }); }); - it('it creates IS_FETCHING_NOTIFICATIONS actions and sends an error ' + - 'notification with on failure', () => { + it('it creates IS_FETCHING_NOTIFICATIONS, GET_MORE_ADMIN_NOTIFICATIONS and SET_NOTICATIONS_PAGINATION when successfully called with offset > 0', + () => { + const { notifications } = mockStoreData.notificationReducer; + const { pagination } = mockStoreData.notificationReducer; + moxios.stubRequest('/api/v1/admin-notifications?offset=20&', { + status: 200, + response: { message: 'success', notifications, metadata: pagination } + }); + const expectedActions = [ + { type: actionTypes.IS_FETCHING_NOTIFICATIONS, status: true }, + { type: actionTypes.SET_NOTICATIONS_PAGINATION, pagination }, + { type: actionTypes.GET_MORE_ADMIN_NOTIFICATIONS, notifications }, + { type: actionTypes.IS_FETCHING_NOTIFICATIONS, status: false } + ]; + const store = mockStore({}); + return store.dispatch(fetchNotifications({ offset: 20 })).then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect(Notify.success).toHaveBeenCalled(); + }); + }); + + it('it creates IS_FETCHING_NOTIFICATIONS actions and sends an error notification with on failure', () => { const { notifications } = mockStoreData.notificationReducer; const { pagination } = mockStoreData.notificationReducer; moxios.stubRequest('/api/v1/admin-notifications', { @@ -192,7 +213,19 @@ describe('ADMIN ACTIONS', () => { const store = mockStore({}); return store.dispatch(fetchNotifications()).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); + }); + }); + }); + + describe('uploadFile', () => { + it('handles file upload success', (done) => { + const store = mockStore({}); + return store.dispatch(uploadFile('file')).then((response) => { + expect(response.ok).toBe(true); + expect(response.status()).toBe(200); + expect(response.data.message).toBe('success'); + done(); }); }); }); diff --git a/client/__tests__/actions/authActions.spec.js b/client/__tests__/actions/authActions.spec.js index 4325e7c..38eda7b 100644 --- a/client/__tests__/actions/authActions.spec.js +++ b/client/__tests__/actions/authActions.spec.js @@ -7,11 +7,11 @@ import mockLocalStorage from '../__mocks__/mockLocalStorage'; import { login } from '../../actions/authActions/login'; import logout from '../../actions/authActions/logout'; import { signUp } from '../../actions/authActions/signup'; -import requestResetPassword from '../../actions/authActions/requestResetPassword'; +import requestResetPassword + from '../../actions/authActions/requestResetPassword'; import resetPassword from '../../actions/authActions/resetPassword'; import actionTypes from '../../actions/actionTypes'; -import notify from '../__mocks__/notify'; -import { request } from 'https'; +import Notify from '../__mocks__/Notify'; const middleware = [thunk]; const mockStore = configureMockStore(middleware); @@ -23,24 +23,24 @@ describe('Auth Actions', () => { afterEach(() => moxios.uninstall()); describe('login', () => { - it('creates LOGIN, AUTH_LOADING and SET_LOGIN_STATUS' + - ' when login is successful', () => { - const { authResponse } = mockData; - moxios.stubRequest('/api/v1/users/signin', { - status: 200, - response: authResponse + it('creates LOGIN, AUTH_LOADING and SET_LOGIN_STATUS when login is successful', + () => { + const { authResponse } = mockData; + moxios.stubRequest('/api/v1/users/signin', { + status: 200, + response: authResponse + }); + const expectedActions = [ + { type: actionTypes.AUTH_LOADING, state: true }, + { type: actionTypes.LOGIN, user: authResponse }, + { type: actionTypes.SET_LOGIN_STATUS, isLoggedIn: true }, + { type: actionTypes.AUTH_LOADING, state: false } + ]; + const store = mockStore({}); + return store.dispatch(login({})).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - const expectedActions = [ - { type: actionTypes.AUTH_LOADING, state: true }, - { type: actionTypes.LOGIN, user: authResponse }, - { type: actionTypes.SET_LOGIN_STATUS, isLoggedIn: true }, - { type: actionTypes.AUTH_LOADING, state: false } - ]; - const store = mockStore({}); - return store.dispatch(login({})).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); it('creates AUTH_LOADING and SET_LOGIN_STATUS on login failure', () => { @@ -61,24 +61,24 @@ describe('Auth Actions', () => { }); describe('signup', () => { - it('creates LOGIN, AUTH_LOADING and SET_LOGIN_STATUS' + - ' when sign up is successful', () => { - const { authResponse } = mockData; - moxios.stubRequest('/api/v1/users/signup', { - status: 200, - response: authResponse - }); - const expectedActions = [ - { type: actionTypes.AUTH_LOADING, state: true }, - { type: actionTypes.SIGN_UP, user: authResponse }, - { type: actionTypes.SET_LOGIN_STATUS, isLoggedIn: true }, - { type: actionTypes.AUTH_LOADING, state: false } - ]; - const store = mockStore({}); - return store.dispatch(signUp({})).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('creates LOGIN, AUTH_LOADING and SET_LOGIN_STATUS when sign up is successful', + () => { + const { authResponse } = mockData; + moxios.stubRequest('/api/v1/users/signup', { + status: 200, + response: authResponse + }); + const expectedActions = [ + { type: actionTypes.AUTH_LOADING, state: true }, + { type: actionTypes.SIGN_UP, user: authResponse }, + { type: actionTypes.SET_LOGIN_STATUS, isLoggedIn: true }, + { type: actionTypes.AUTH_LOADING, state: false } + ]; + const store = mockStore({}); + return store.dispatch(signUp({})).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); it('creates AUTH_LOADING on signup failure', () => { const { authResponse } = mockData; moxios.stubRequest('/api/v1/users/signup', { @@ -107,50 +107,54 @@ describe('Auth Actions', () => { }); describe('requestResetPassword', () => { - it('provides a notification on success', () => { + it('provides a notification on success', (done) => { moxios.stubRequest('/api/v1/users/forgot-password', { status: 200, response: mockData.authResponse }); - expect(notify.success).toHaveBeenCalled(); - }); - - it("provides a notification on success", () => { - window.requestResetPassword = () => () => Promise.resolve(1); - requestResetPassword("password", "token"); - expect(notify.success).toHaveBeenCalled(); + const store = mockStore({}); + store.dispatch(requestResetPassword('email')).then(() => { + expect(Notify.success).toHaveBeenCalled(); + done(); + }); }); - it('provides a notification on failure', () => { + it('provides a notification on failure', (done) => { moxios.stubRequest('/api/v1/users/forgot-password', { status: 500, response: mockData.authResponse }); - expect(notify.error).toHaveBeenCalled(); + const store = mockStore({}); + store.dispatch(requestResetPassword('email')).then(() => { + expect(Notify.error).toHaveBeenCalled(); + done(); + }); }); }); describe('resetPassword', () => { - it('provides a notification on success', () => { + it('provides a notification on success', (done) => { moxios.stubRequest('/api/v1/users/reset-password/1234yyjhkopi123', { status: 200, response: mockData.authResponse }); - expect(notify.success).toHaveBeenCalled(); - }); - - it('provides a notification on success', () => { - window.resetPassword = () => () => Promise.resolve(1); - resetPassword('password', 'token'); - expect(notify.success).toHaveBeenCalled(); + const store = mockStore({}); + store.dispatch(resetPassword('password', '1234yyjhkopi123')).then(() => { + expect(Notify.success).toHaveBeenCalled(); + done(); + }); }); - it('provides a notification on failure', () => { + it('provides a notification on failure', (done) => { moxios.stubRequest('/api/v1/users/reset-password/1234yyjhkopi123', { status: 500, response: mockData.authResponse }); - expect(notify.error).toHaveBeenCalled(); + const store = mockStore({}); + store.dispatch(resetPassword('password', '1234yyjhkopi123')).then(() => { + expect(Notify.error).toHaveBeenCalled(); + done(); + }); }); }); }); diff --git a/client/__tests__/actions/bookActions.spec.js b/client/__tests__/actions/bookActions.spec.js index d95d1ec..5ef0e7f 100644 --- a/client/__tests__/actions/bookActions.spec.js +++ b/client/__tests__/actions/bookActions.spec.js @@ -5,13 +5,17 @@ import configureMockStore from 'redux-mock-store'; import { mockStoreData } from '../__mocks__/mockData'; import { fetchBorrowedBooks, returnBook, - fetchBorrowingHistory } from '../../actions/bookActions/borrowedBooks'; + fetchBorrowingHistory, + getSuggestedBooks, + readBook +} from '../../actions/bookActions/borrowedBooks'; import { fetchBooks, borrowBook, getBookCategories, filterBooksByCategory } from '../../actions/bookActions/library'; import { viewBookDetails } from '../../actions/bookActions/viewBook'; import actionTypes from '../../actions/actionTypes'; +import Notify from '../__mocks__/Notify'; const middleware = [thunk]; const mockStore = configureMockStore(middleware); @@ -140,77 +144,77 @@ describe('Book Actions', () => { }); describe('fetchBooks', () => { - it('dispatches GET_BOOK, FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION ' + - 'action', () => { - const { books } = mockStoreData.bookReducer; - const { pagination } = mockStoreData.bookReducer; - moxios.stubRequest('/api/v1/books', { - status: 200, - response: { books, metadata: pagination } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, - { type: actionTypes.FETCHING_MORE_BOOKS, status: false }, - { type: actionTypes.GET_BOOKS, books }, - { type: actionTypes.SET_LIBRARY_PAGINATION, pagination }, - ]; - const store = mockStore({}); - return store.dispatch(fetchBooks()).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches GET_BOOK, FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION action', + () => { + const { books } = mockStoreData.bookReducer; + const { pagination } = mockStoreData.bookReducer; + moxios.stubRequest('/api/v1/books', { + status: 200, + response: { books, metadata: pagination } + }); + const expectedActions = [ + { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, + { type: actionTypes.FETCHING_MORE_BOOKS, status: false }, + { type: actionTypes.GET_BOOKS, books }, + { type: actionTypes.SET_LIBRARY_PAGINATION, pagination }, + ]; + const store = mockStore({}); + return store.dispatch(fetchBooks()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); - it('dispatches GET_MORE_BOOKS, FETCHING_MORE_BOOKS and ' + - 'SET_LIBRARY_PAGINATION actions if offset is greater than 0', () => { - const { books } = mockStoreData.bookReducer; - const { pagination } = mockStoreData.bookReducer; - moxios.stubRequest('/api/v1/books?offset=4&', { - status: 200, - response: { books, metadata: pagination } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, - { type: actionTypes.FETCHING_MORE_BOOKS, status: false }, - { type: actionTypes.GET_MORE_BOOKS, books }, - { type: actionTypes.SET_LIBRARY_PAGINATION, pagination }, - ]; - const store = mockStore({}); - return store.dispatch(fetchBooks({ offset: 4 })).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches GET_MORE_BOOKS, FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION actions if offset is greater than 0', + () => { + const { books } = mockStoreData.bookReducer; + const { pagination } = mockStoreData.bookReducer; + moxios.stubRequest('/api/v1/books?offset=4&', { + status: 200, + response: { books, metadata: pagination } + }); + const expectedActions = [ + { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, + { type: actionTypes.FETCHING_MORE_BOOKS, status: false }, + { type: actionTypes.GET_MORE_BOOKS, books }, + { type: actionTypes.SET_LIBRARY_PAGINATION, pagination }, + ]; + const store = mockStore({}); + return store.dispatch(fetchBooks({ offset: 4 })).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); - it('dispatches only FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION ' + - 'actions if no books were found', () => { - moxios.stubRequest('/api/v1/books', { - status: 200, - response: { books: [] } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, - { type: actionTypes.FETCHING_MORE_BOOKS, status: false } - ]; - const store = mockStore({}); - return store.dispatch(fetchBooks()).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches only FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION actions if no books were found', + () => { + moxios.stubRequest('/api/v1/books', { + status: 200, + response: { books: [] } + }); + const expectedActions = [ + { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, + { type: actionTypes.FETCHING_MORE_BOOKS, status: false } + ]; + const store = mockStore({}); + return store.dispatch(fetchBooks()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); - it('dispatches only FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION ' + - 'actions on failure', () => { - moxios.stubRequest('/api/v1/books?limit=2&', { - status: 400, - response: {} - }); - const expectedActions = [ - { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, - { type: actionTypes.FETCHING_MORE_BOOKS, status: false } - ]; - const store = mockStore({}); - return store.dispatch(fetchBooks({ limit: 2 })).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches only FETCHING_MORE_BOOKS and SET_LIBRARY_PAGINATION actions on failure', + () => { + moxios.stubRequest('/api/v1/books?limit=2&', { + status: 400, + response: {} + }); + const expectedActions = [ + { type: actionTypes.FETCHING_MORE_BOOKS, status: true }, + { type: actionTypes.FETCHING_MORE_BOOKS, status: false } + ]; + const store = mockStore({}); + return store.dispatch(fetchBooks({ limit: 2 })).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); }); describe('borrowBook', () => { @@ -297,4 +301,45 @@ describe('Book Actions', () => { }); }); }); + + describe('getSuggestedBooks', () => { + it('dispatches GET_BOOK_SUGGESTIONS on success', () => { + const suggestions = [{ cover: 'some cover', title: 'title' }]; + moxios.stubRequest('/api/v1/books/suggestions', { + status: 200, + response: { suggestions } + }); + const expectedActions = [ + { type: actionTypes.GET_BOOK_SUGGESTIONS, suggestions } + ]; + const store = mockStore({}); + return store.dispatch(getSuggestedBooks()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it('it dispatches no actions but calls Notify.error on failure', () => { + moxios.stubRequest('/api/v1/books/suggestions', { + status: 500, + response: { message: 'failure' } + }); + const expectedActions = []; + const store = mockStore({}); + return store.dispatch(getSuggestedBooks()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect(Notify.error).toHaveBeenCalled(); + }); + }); + }); + + describe('readBook', () => { + it('dispatches SET_BOOK_TO_READ', (done) => { + const url = 'something-made-up'; + const expectedActions = [{ type: actionTypes.SET_BOOK_TO_READ, url }]; + const store = mockStore({}); + store.dispatch(readBook(url)); + expect(store.getActions()).toEqual(expectedActions); + done(); + }); + }); }); diff --git a/client/__tests__/actions/userActions.spec.js b/client/__tests__/actions/userActions.spec.js index 573ac94..1688be1 100644 --- a/client/__tests__/actions/userActions.spec.js +++ b/client/__tests__/actions/userActions.spec.js @@ -8,7 +8,7 @@ import actionTypes from '../../actions/actionTypes'; import updateProfile from '../../actions/updateProfile'; import { fetchHistory, fetchTransactionHistory } from '../../actions/history'; -import notify from '../__mocks__/notify'; +import Notify from '../__mocks__/Notify'; const middleware = [thunk]; const mockStore = configureMockStore(middleware); @@ -29,7 +29,7 @@ describe('user Actions', () => { const store = mockStore({}); return store.dispatch(updateProfile({ limit: 2 })).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.success).toHaveBeenCalled(); + expect(Notify.success).toHaveBeenCalled(); }); }); @@ -43,29 +43,29 @@ describe('user Actions', () => { const store = mockStore({}); return store.dispatch(updateProfile({})).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); describe('fetchHistory', () => { const { allBorrowed } = mockStoreData.transactionReducer; - it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and a success toast ' + - 'on success', () => { - moxios.stubRequest('/api/v1/users/1/books', { - status: 200, - response: { books: allBorrowed } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_HISTORY, status: true }, - { type: actionTypes.FETCHING_HISTORY, status: false }, - { type: actionTypes.GET_ALL_BORROWED, books: allBorrowed }, - ]; - const store = mockStore({}); - return store.dispatch(fetchHistory(1)).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and a success toast on success', + () => { + moxios.stubRequest('/api/v1/users/1/books', { + status: 200, + response: { books: allBorrowed } + }); + const expectedActions = [ + { type: actionTypes.FETCHING_HISTORY, status: true }, + { type: actionTypes.FETCHING_HISTORY, status: false }, + { type: actionTypes.GET_ALL_BORROWED, books: allBorrowed }, + ]; + const store = mockStore({}); + return store.dispatch(fetchHistory(1)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); it('returns an error toast on failure', () => { moxios.stubRequest('/api/v1/users/1/books', { @@ -79,7 +79,7 @@ describe('user Actions', () => { const store = mockStore({}); return store.dispatch(fetchHistory(1)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); @@ -87,42 +87,42 @@ describe('user Actions', () => { describe('fetchTransactionHistory', () => { const { transactions, pagination } = mockStoreData.transactionReducer; - it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and ' + - 'SET_TRANSACTIONS_PAGINATION on success', () => { - moxios.stubRequest('/api/v1/users/1/transactions', { - status: 200, - response: { notifications: transactions, metadata: pagination } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_TRANSACTIONS, status: true }, - { type: actionTypes.FETCHING_TRANSACTIONS, status: false }, - { type: actionTypes.GET_TRANSACTION_HISTORY, transactions }, - { type: actionTypes.SET_TRANSACTIONS_PAGINATION, pagination }, - ]; - const store = mockStore({}); - return store.dispatch(fetchTransactionHistory(null, 1)).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and SET_TRANSACTIONS_PAGINATION on success', + () => { + moxios.stubRequest('/api/v1/users/1/transactions', { + status: 200, + response: { notifications: transactions, metadata: pagination } + }); + const expectedActions = [ + { type: actionTypes.FETCHING_TRANSACTIONS, status: true }, + { type: actionTypes.FETCHING_TRANSACTIONS, status: false }, + { type: actionTypes.GET_TRANSACTION_HISTORY, transactions }, + { type: actionTypes.SET_TRANSACTIONS_PAGINATION, pagination }, + ]; + const store = mockStore({}); + return store.dispatch(fetchTransactionHistory(null, 1)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); }); - }); - it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and ' + - 'SET_TRANSACTIONS_PAGINATION on success', () => { - moxios.stubRequest('/api/v1/users/1/transactions?offset=2&', { - status: 200, - response: { notifications: transactions, metadata: pagination } - }); - const expectedActions = [ - { type: actionTypes.FETCHING_TRANSACTIONS, status: true }, - { type: actionTypes.FETCHING_TRANSACTIONS, status: false }, - { type: actionTypes.GET_MORE_TRANSACTIONS, transactions }, - { type: actionTypes.SET_TRANSACTIONS_PAGINATION, pagination }, - ]; - const store = mockStore({}); - return store.dispatch(fetchTransactionHistory({ offset: 2 }, 1)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); + it('dispatches FETCHING_HISTORY, GET_ALL_BORROWED and SET_TRANSACTIONS_PAGINATION on success', + () => { + moxios.stubRequest('/api/v1/users/1/transactions?offset=2&', { + status: 200, + response: { notifications: transactions, metadata: pagination } }); - }); + const expectedActions = [ + { type: actionTypes.FETCHING_TRANSACTIONS, status: true }, + { type: actionTypes.FETCHING_TRANSACTIONS, status: false }, + { type: actionTypes.GET_MORE_TRANSACTIONS, transactions }, + { type: actionTypes.SET_TRANSACTIONS_PAGINATION, pagination }, + ]; + const store = mockStore({}); + return store.dispatch(fetchTransactionHistory({ offset: 2 }, 1)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); it('returns an error toast on failure', () => { moxios.stubRequest('/api/v1/users/1/transactions', { @@ -136,7 +136,7 @@ describe('user Actions', () => { const store = mockStore({}); return store.dispatch(fetchTransactionHistory(null, 1)).then(() => { expect(store.getActions()).toEqual(expectedActions); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); }); diff --git a/client/__tests__/components/Admin/CategoryForm.spec.jsx b/client/__tests__/components/Admin/AddCategoryForm.spec.jsx similarity index 82% rename from client/__tests__/components/Admin/CategoryForm.spec.jsx rename to client/__tests__/components/Admin/AddCategoryForm.spec.jsx index 868dba1..30852f8 100644 --- a/client/__tests__/components/Admin/CategoryForm.spec.jsx +++ b/client/__tests__/components/Admin/AddCategoryForm.spec.jsx @@ -1,16 +1,16 @@ import React from 'react'; import { shallow } from 'enzyme'; -import CategoryForm from '../../../components/Admin/CategoryForm'; +import AddCategoryForm from '../../../components/Admin/AddCategoryForm'; const props = { className: 'foo', onSubmit: jest.fn(), }; -const setUp = () => (shallow()); +const setUp = () => (shallow()); -describe('Category Form Component', () => { +describe('AddCategoryForm Component', () => { const wrapper = setUp(); it('renders without crashing', () => { expect(wrapper).toBeDefined(); diff --git a/client/__tests__/components/Admin/BookForm.spec.jsx b/client/__tests__/components/Admin/BookForm.spec.jsx index 6a99823..dc9c070 100644 --- a/client/__tests__/components/Admin/BookForm.spec.jsx +++ b/client/__tests__/components/Admin/BookForm.spec.jsx @@ -10,8 +10,7 @@ const props = { book: mockStoreData.bookReducer.books[0], imageUploading: false, imageUploaded: false, - imageError: '', - bookFileError: '', + errors: {}, bookFileUploading: false, bookFileUploaded: false, onBookFileChange: jest.fn(), @@ -34,30 +33,33 @@ describe('BookForm Component', () => { expect(wrapper.find('form').length).toBe(1); }); - it('calls onSelectCategory prop function when a category is selected', () => { - const submitBtn = wrapper.find('Categories').at(0); - submitBtn.simulate('change'); - expect(props.onSelectCategory).toHaveBeenCalled(); - }); + it('calls onSelectCategory prop function when a category is selected', + () => { + const submitBtn = wrapper.find('Categories').at(0); + submitBtn.simulate('change'); + expect(props.onSelectCategory).toHaveBeenCalled(); + }); - it('calls onSubmit prop function when submit button is clicked', () => { - const submitBtn = wrapper.find('input[type="submit"]').at(0); - submitBtn.simulate('click'); - expect(props.onSubmit).toHaveBeenCalled(); - }); + it('calls onSubmit prop function when submit button is clicked', + () => { + const submitBtn = wrapper.find('input[type="submit"]').at(0); + submitBtn.simulate('click'); + expect(props.onSubmit).toHaveBeenCalled(); + }); - it('calls onBookFileChange prop function when book is loaded to form', () => { - const bookFileInput = wrapper.find('input[name="bookFile"]').at(0); - bookFileInput.simulate('change'); - expect(props.onBookFileChange).toHaveBeenCalled(); - }); + it('calls onBookFileChange prop function when book is loaded to form', + () => { + const bookFileInput = wrapper.find('input[name="bookFile"]').at(0); + bookFileInput.simulate('change'); + expect(props.onBookFileChange).toHaveBeenCalled(); + }); - it('calls onBookConverChange prop function when ' + - 'book cover is loaded to form', () => { - const bookCoverInput = wrapper.find('input[name="cover"]').at(0); - bookCoverInput.simulate('change'); - expect(props.onBookConverChange).toHaveBeenCalled(); - }); + it('calls onBookConverChange prop function when book cover is loaded to form', + () => { + const bookCoverInput = wrapper.find('input[name="cover"]').at(0); + bookCoverInput.simulate('change'); + expect(props.onBookConverChange).toHaveBeenCalled(); + }); it('calls onChange prop function when form fields change value', () => { const textInput = wrapper.find('input[type="text"]').at(0); diff --git a/client/__tests__/components/Admin/index.spec.jsx b/client/__tests__/components/Admin/index.spec.jsx index af1958e..23fcdcb 100644 --- a/client/__tests__/components/Admin/index.spec.jsx +++ b/client/__tests__/components/Admin/index.spec.jsx @@ -16,14 +16,14 @@ const props = { categories: mockStoreData.bookReducer.categories, notifications: mockStoreData.notificationReducer.notifications, pagination: mockStoreData.notificationReducer.pagination, - addBook: jest.fn(), - editBook: jest.fn(), + addBook: jest.fn(() => Promise.resolve(1)), + editBook: jest.fn(() => Promise.resolve(1)), getBookCategories: jest.fn(), fetchNotifications: jest.fn(), addBookCategory: jest.fn(), history: { push: jest.fn() }, location: { pathname: '/admin/edit' }, - uploadFile: jest.fn(() => ({ end: jest.fn(() => Promise.resolve(1)) })), + uploadFile: jest.fn(() => Promise.resolve(1)), }; const setUp = () => (shallow()); @@ -61,6 +61,7 @@ describe('Admin Component', () => { }; const wrapper = shallow(); expect(wrapper.find('BookForm').props().book.title).toBe(''); + expect(wrapper.find('BookForm').props().book.description).toBe(''); expect(wrapper.find('BookForm').props().book.total).toBe(0); }); @@ -165,6 +166,68 @@ describe('Admin Component', () => { expect(wrapper.state().errors).toEqual({}); }); + it('should call handleFormSubmission on form submission for editing book', + () => { + const wrapper = setUp(); + const handleFormSubmissionSpy = jest.spyOn( + wrapper.instance(), 'handleFormSubmission' + ); + const event = { + preventDefault: jest.fn(), + }; + expect(wrapper.state().book.bookFile).toBe(''); + + wrapper.instance().handleFormSubmission(event); + expect(handleFormSubmissionSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().book.bookFile).toBe(''); + expect(wrapper.state().book.total).toBe(props.book.total); + expect(wrapper.state().book.categoryId).toBe(0); + }); + + it('should set error state if handleFormSubmission is called with invalid data', + () => { + const addBookProps = { ...props, location: { pathname: '/admin' } }; + const wrapper = shallow(); + const handleFormSubmissionSpy = jest.spyOn( + wrapper.instance(), 'handleFormSubmission' + ); + const event = { + preventDefault: jest.fn(), + }; + wrapper.instance().handleFormSubmission(event); + expect(handleFormSubmissionSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().errors.title).toBe('Book must have a title'); + expect(wrapper.state().errors.authors) + .toBe('Book must have at least one author'); + expect(wrapper.state().book.categoryId).toBe(0); + }); + + it('should call handleFormSubmission on form submission for adding book', + () => { + const addBookProps = { + ...props, + book: { ...props.book, title: 'diffing algorithms' }, + location: { pathname: '/admin' } + }; + const wrapper = shallow(); + wrapper.setState({ + book: { ...wrapper.state().books, ...addBookProps.book } + }); + + const handleFormSubmissionSpy = jest.spyOn( + wrapper.instance(), 'handleFormSubmission' + ); + const event = { + preventDefault: jest.fn(), + }; + wrapper.instance().handleFormSubmission(event); + expect(handleFormSubmissionSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().book.total).toBe(10); + expect(wrapper.state().book.title).toBe('diffing algorithms'); + expect(wrapper.state().book.cover) + .toBe('https://image/upload/cloudinary-stub/to2ila7jbe.jpg'); + }); + it('should call handleSelectCategory when a category is selected', () => { const wrapper = setUp(); const handleSelectCategorySpy = jest.spyOn( diff --git a/client/__tests__/components/Dashboard/Borrowed.spec.jsx b/client/__tests__/components/Dashboard/Borrowed.spec.jsx index ed29b1d..2cba36f 100644 --- a/client/__tests__/components/Dashboard/Borrowed.spec.jsx +++ b/client/__tests__/components/Dashboard/Borrowed.spec.jsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import Borrowed from '../../../components/Dashboard/Borrowed'; import { mockStoreData } from '../../__mocks__/mockData'; +window.BOOK_IMAGE_FALLBACK = 'fallback'; const props = { borrowedBooks: mockStoreData.bookReducer.borrowedBooks, diff --git a/client/__tests__/components/Dashboard/PdfViewer.spec.jsx b/client/__tests__/components/Dashboard/PdfViewer.spec.jsx new file mode 100644 index 0000000..3840a17 --- /dev/null +++ b/client/__tests__/components/Dashboard/PdfViewer.spec.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import ConnectedPdfViewer, { PdfViewer } + from '../../../components/Dashboard/PdfViewer'; +import { mockStoreData } from '../../__mocks__/mockData'; + +jest.mock('react-pdf-js'); +window.BOOK_FALLBACK = 'some-url'; + +const props = { + bookUrl: 'some-book-url', + isLoggedIn: true, +}; + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); +const store = mockStore({ ...mockStoreData }); + +const setUp = () => shallow(); + +describe('PdfViewer Component', () => { + it('renders without crashing', () => { + const wrapper = setUp(); + 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 onDocumentComplete when file is loaded', () => { + const wrapper = setUp(); + const onDocumentCompleteSpy = jest.spyOn( + wrapper.instance(), 'onDocumentComplete' + ); + wrapper.instance().onDocumentComplete(44); + expect(onDocumentCompleteSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().pages).toBe(44); + }); + + it('should call onPageComplete when file is loaded', () => { + const wrapper = setUp(); + const onPageCompleteSpy = jest.spyOn( + wrapper.instance(), 'onPageComplete' + ); + wrapper.instance().onPageComplete(1); + expect(onPageCompleteSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().page).toBe(1); + }); + + it('should call setPage when page is manually inserted', () => { + const wrapper = setUp(); + const setPageSpy = jest.spyOn( + wrapper.instance(), 'setPage' + ); + const event = { + nextentDefault: jest.fn(), + target: { + value: '55' + } + }; + wrapper.instance().setPage(event); + expect(setPageSpy).toHaveBeenCalledTimes(1); + expect(wrapper.state().page).toBe(55); + }); + + it('should decrement page by one when handlePrevious is called', () => { + const wrapper = setUp(); + wrapper.setState({ page: 2 }); + wrapper.instance().handlePrevious(); + expect(wrapper.state().page).toBe(1); + }); + + it('should increment page by one when handlePrevious is called', () => { + const wrapper = setUp(); + wrapper.setState({ page: 4 }); + wrapper.instance().handleNext(); + expect(wrapper.state().page).toBe(5); + }); +}); diff --git a/client/__tests__/components/Dashboard/SuggestedBooks.spec.jsx b/client/__tests__/components/Dashboard/SuggestedBooks.spec.jsx index b682569..2582f4d 100644 --- a/client/__tests__/components/Dashboard/SuggestedBooks.spec.jsx +++ b/client/__tests__/components/Dashboard/SuggestedBooks.spec.jsx @@ -10,6 +10,8 @@ const props = { suggestedBooks: mockStoreData.bookReducer.borrowedBooks, }; +window.BOOK_IMAGE_FALLBACK = 'fallback'; + describe('SuggestedBooks component', () => { const wrapper = shallow(); diff --git a/client/__tests__/components/Dashboard/index.spec.jsx b/client/__tests__/components/Dashboard/index.spec.jsx index ce1708e..5983519 100644 --- a/client/__tests__/components/Dashboard/index.spec.jsx +++ b/client/__tests__/components/Dashboard/index.spec.jsx @@ -16,6 +16,8 @@ let props = { fetchBorrowedBooks: jest.fn(), returnBook: jest.fn(), readBook: jest.fn(), + suggestions: [], + getSuggestedBooks: jest.fn(), ...mockStoreData.authReducer, ...mockStoreData.bookReducer.borrowedBooks, fetchingBorrowedBooks: false @@ -54,16 +56,16 @@ describe('Dashboard Component', () => { expect(componentDidMountSpy).toHaveBeenCalledTimes(1); }); - it("should call the handleReturnBook method", () => { + it('should call the handleReturnBook method', () => { const wrapper = shallow(); - const handleReturnBookSpy = jest.spyOn(wrapper.instance(), "handleReturnBook"); + const handleReturnBookSpy = jest.spyOn(wrapper.instance(), 'handleReturnBook'); wrapper.instance().handleReturnBook(1); expect(handleReturnBookSpy).toHaveBeenCalledTimes(1); }); - it("should call the readBook method", () => { + it('should call the readBook method', () => { const wrapper = shallow(); - const readBookSpy = jest.spyOn(wrapper.instance(), "readBook"); + const readBookSpy = jest.spyOn(wrapper.instance(), 'readBook'); wrapper.instance().readBook(1); expect(readBookSpy).toHaveBeenCalledTimes(1); }); diff --git a/client/__tests__/components/Dashboard/updateProfile.spec.jsx b/client/__tests__/components/Dashboard/updateProfile.spec.jsx index 48142ca..e860f45 100644 --- a/client/__tests__/components/Dashboard/updateProfile.spec.jsx +++ b/client/__tests__/components/Dashboard/updateProfile.spec.jsx @@ -27,7 +27,7 @@ describe('UpdateProfile Component', () => { props = { ...props, isLoggedIn: false }; const wrapper = shallow(); expect(wrapper).toBeDefined(); - expect(wrapper.find('Redirect').length).toBe(1); + expect(wrapper.find('LoginRedirect').length).toBe(1); expect(wrapper.find('div').length).toBe(0); }); diff --git a/client/__tests__/components/History/BorrowedTable.spec.jsx b/client/__tests__/components/History/BorrowedTable.spec.jsx index 15c9ad9..d5e840b 100644 --- a/client/__tests__/components/History/BorrowedTable.spec.jsx +++ b/client/__tests__/components/History/BorrowedTable.spec.jsx @@ -3,6 +3,8 @@ import { shallow } from 'enzyme'; import BorrowedTable from '../../../components/History/BorrowedTable'; import { mockStoreData } from '../../__mocks__/mockData'; +window.BOOK_IMAGE_FALLBACK = 'fallback'; + const props = { books: mockStoreData.transactionReducer.allBorrowed }; describe('BorrowedTable Component', () => { diff --git a/client/__tests__/components/History/index.spec.jsx b/client/__tests__/components/History/index.spec.jsx index c8f9eb7..97076df 100644 --- a/client/__tests__/components/History/index.spec.jsx +++ b/client/__tests__/components/History/index.spec.jsx @@ -70,12 +70,12 @@ describe('History Component', () => { expect(wrapper.find('AllBorrowed').length).toBe(1); }); - it('renders one TransactionHistory Component when url changes to ' + - '/history/transactions', () => { - const viewTransactions = { ...props, location }; - const wrapper = shallow(); - expect(wrapper.find('TransactionHistory').length).toBe(1); - }); + it('renders one TransactionHistory Component when url changes to /history/transactions', + () => { + const viewTransactions = { ...props, location }; + const wrapper = shallow(); + expect(wrapper.find('TransactionHistory').length).toBe(1); + }); it('should render the connected component', () => { const connectedComponent = shallow( diff --git a/client/__tests__/components/Library/BooksTable.spec.jsx b/client/__tests__/components/Library/BooksTable.spec.jsx index 13c1a0c..7b954e9 100644 --- a/client/__tests__/components/Library/BooksTable.spec.jsx +++ b/client/__tests__/components/Library/BooksTable.spec.jsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import BooksTable from '../../../components/Library/BooksTable'; import { mockStoreData } from '../../__mocks__/mockData'; +window.BOOK_IMAGE_FALLBACK = 'fallback'; const props = { borrowBook: jest.fn(), diff --git a/client/__tests__/components/auth/Login.spec.jsx b/client/__tests__/components/auth/Login.spec.jsx index c29807b..1cc6f91 100644 --- a/client/__tests__/components/auth/Login.spec.jsx +++ b/client/__tests__/components/auth/Login.spec.jsx @@ -41,14 +41,14 @@ describe('Login Component', () => { expect(wrapper.find('form').length).toBe(1); }); - it('renders three input fields of type text, password and ' + - 'submit respectivey', () => { - const wrapper = setUp(); - expect(wrapper.find('input').length).toBe(3); - expect(wrapper.find('input').at(0).props().type).toBe('text'); - expect(wrapper.find('input').at(1).props().type).toBe('password'); - expect(wrapper.find('input').at(2).props().type).toBe('submit'); - }); + it('renders three input fields of type text, password and submit respectivey', + () => { + const wrapper = setUp(); + expect(wrapper.find('input').length).toBe(3); + expect(wrapper.find('input').at(0).props().type).toBe('text'); + expect(wrapper.find('input').at(1).props().type).toBe('password'); + expect(wrapper.find('input').at(2).props().type).toBe('submit'); + }); it('should redirect to Dashboard if user is logged in already', () => { const loggedinProps = { ...props, isLoggedIn: true }; diff --git a/client/__tests__/components/auth/LoginRedirect.spec.jsx b/client/__tests__/components/auth/LoginRedirect.spec.jsx index 41ed0eb..6d7286b 100644 --- a/client/__tests__/components/auth/LoginRedirect.spec.jsx +++ b/client/__tests__/components/auth/LoginRedirect.spec.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import LoginRedirect from '../../../components/auth/LoginRedirect'; -import notify from '../../__mocks__/notify'; +import Notify from '../../__mocks__/Notify'; jest.mock('react-router-dom'); @@ -12,6 +12,6 @@ describe('LoginRedirect', () => { expect(wrapper).toBeDefined(); expect(wrapper.find('Redirect').length).toBe(1); expect(wrapper.find('Redirect').props().to).toBe('/login'); - expect(notify.error).toHaveBeenCalled(); + expect(Notify.error).toHaveBeenCalled(); }); }); diff --git a/client/__tests__/components/auth/Password.spec.jsx b/client/__tests__/components/auth/Password.spec.jsx index 8e071c1..f242eb9 100644 --- a/client/__tests__/components/auth/Password.spec.jsx +++ b/client/__tests__/components/auth/Password.spec.jsx @@ -26,11 +26,11 @@ describe('Password Component', () => { expect(wrapper.getElement().type).toBe('div'); }); - it('should render a ResetPasswordForm and not ForgotPasswordForm ' + - 'Component if url matches "/reset-password"', () => { - expect(wrapper.find('ResetPasswordForm').length).toBe(1); - expect(wrapper.find('ForgotPasswordForm').length).toBe(0); - }); + it('should render a ResetPasswordForm and not ForgotPasswordForm Component if url matches "/reset-password"', + () => { + expect(wrapper.find('ResetPasswordForm').length).toBe(1); + expect(wrapper.find('ForgotPasswordForm').length).toBe(0); + }); it('should redirect to dashboard if user is logged in', () => { const wrongUrlProps = { ...props, isLoggedIn: true }; @@ -53,74 +53,105 @@ describe('Password Component', () => { expect(wrongUrlWrapper.find('Redirect').props().to).toBe('/'); }); - it('should call handleResetPassword when ForgotPasswordForm ' + - 'is submitted', () => { - const forgotProps = { - ...props, - match: { ...props.match, url: '/reset-password' } - }; - const renderReset = shallow(); - const handleResetPasswordSpy = jest.spyOn(renderReset.instance(), - 'handleResetPassword'); - const event = { - preventDefault: jest.fn(), - target: { - password: { value: 'funny-secret' }, - confirmPassword: { value: 'funny-secret' }, - } - }; - renderReset.instance().handleResetPassword(event); - expect(handleResetPasswordSpy).toHaveBeenCalledTimes(1); - expect(renderReset.state().loading).toBe(true); - }); + it('should call handleResetPassword when ForgotPasswordForm is submitted', + () => { + const forgotProps = { + ...props, + match: { ...props.match, url: '/reset-password' } + }; + const renderReset = shallow(); + const handleResetPasswordSpy = jest.spyOn(renderReset.instance(), + 'handleResetPassword'); + const event = { + preventDefault: jest.fn(), + target: { + password: { value: 'funny-secret' }, + confirmPassword: { value: 'funny-secret' }, + } + }; + renderReset.instance().handleResetPassword(event); + expect(handleResetPasswordSpy).toHaveBeenCalledTimes(1); + expect(renderReset.state().loading).toBe(true); + }); - it('should give an error message when ForgotPasswordForm ' + - 'is submitted with m=non-matching passwords', () => { - const forgotProps = { - ...props, - match: { ...props.match, url: '/reset-password' } - }; - const renderReset = shallow(); - const event = { - preventDefault: jest.fn(), - target: { - password: { value: 'funny-secret' }, - confirmPassword: { value: 'boring-secret' }, - } - }; - renderReset.instance().handleResetPassword(event); - expect(renderReset.state().loading).toBe(false); - expect(renderReset.state().errors.password).toBe('passwords don\'t match'); - }); + it('should call handleChange when form field values change', + () => { + const handleChangeSpy = jest.spyOn(wrapper.instance(), 'handleChange'); + const event = { + preventDefault: jest.fn(), + target: { name: 'email', value: 'email' } + }; + wrapper.instance().handleChange(event); + expect(handleChangeSpy).toHaveBeenCalledTimes(1); + }); - it('should render a ForgotPasswordForm and not ResetPasswordForm ' + - 'Component if url matches "/forgot-password"', () => { - const forgotProps = { - ...props, - match: { ...props.match, url: '/forgot-password' } - }; - const renderForgot = shallow(); - expect(renderForgot.find('ResetPasswordForm').length).toBe(0); - expect(renderForgot.find('ForgotPasswordForm').length).toBe(1); - }); + it('should give an error message when ForgotPasswordForm is submitted with m=non-matching passwords', + () => { + const forgotProps = { + ...props, + match: { ...props.match, url: '/reset-password' } + }; + const renderReset = shallow(); + const event = { + preventDefault: jest.fn(), + target: { + password: { value: 'funny-secret' }, + confirmPassword: { value: 'boring-secret' }, + } + }; + renderReset.instance().handleResetPassword(event); + expect(renderReset.state().loading).toBe(false); + expect(renderReset.state().errors.password).toBe('passwords don\'t match'); + }); - it('should call handleForgotPassword when ForgotPasswordForm ' + - 'is submitted', () => { - const forgotProps = { - ...props, - match: { ...props.match, url: '/forgot-password' } - }; - const renderForgot = shallow(); - const handleForgotPasswordSpy = jest.spyOn( - renderForgot.instance(), 'handleForgotPassword' - ); - const event = { - preventDefault: jest.fn(), - target: { email: { value: 'some-email' } } - }; - renderForgot.instance().handleForgotPassword(event); - expect(handleForgotPasswordSpy).toHaveBeenCalledTimes(1); - }); + it('should render a ForgotPasswordForm and not ResetPasswordForm Component if url matches "/forgot-password"', + () => { + const forgotProps = { + ...props, + match: { ...props.match, url: '/forgot-password' } + }; + const renderForgot = shallow(); + expect(renderForgot.find('ResetPasswordForm').length).toBe(0); + expect(renderForgot.find('ForgotPasswordForm').length).toBe(1); + }); + + it('should call handleForgotPassword when ForgotPasswordForm is submitted', + () => { + const forgotProps = { + ...props, + match: { ...props.match, url: '/forgot-password' } + }; + const renderForgot = shallow(); + const handleForgotPasswordSpy = jest.spyOn( + renderForgot.instance(), 'handleForgotPassword' + ); + const event = { + preventDefault: jest.fn(), + target: { name: 'email', value: 'some@some.com' } + }; + renderForgot.setState({ email: 'some@some.com' }); + renderForgot.instance().handleForgotPassword(event); + expect(handleForgotPasswordSpy).toHaveBeenCalledTimes(1); + }); + + it('should set error state if handleForgotPassword is called with invalid data', + () => { + const forgotProps = { + ...props, + match: { ...props.match, url: '/forgot-password' } + }; + const renderForgot = shallow(); + const handleForgotPasswordSpy = jest.spyOn( + renderForgot.instance(), 'handleForgotPassword' + ); + const event = { + preventDefault: jest.fn(), + target: { name: 'email', value: 'some-email' } + }; + renderForgot.instance().handleForgotPassword(event); + expect(handleForgotPasswordSpy).toHaveBeenCalledTimes(1); + expect(renderForgot.state().errors.email).toEqual('Email is required'); + }); it('should render the connected component', () => { const connectedComponent = shallow( diff --git a/client/__tests__/components/common/Modal.spec.jsx b/client/__tests__/components/common/Modal.spec.jsx index 3a34ee3..cd3c967 100644 --- a/client/__tests__/components/common/Modal.spec.jsx +++ b/client/__tests__/components/common/Modal.spec.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import Modal from "../../../components/common/Modal"; +import Modal from '../../../components/common/Modal'; const props = { title: 'title', @@ -17,13 +17,13 @@ const props = { describe('Modal', () => { const wrapper = shallow(); it('renders without crashing', () => { - expect(wrapper.getElement().type).toBe("div"); - expect(wrapper.find("div").length).toBeGreaterThan(1); + expect(wrapper.getElement().type).toBe('div'); + expect(wrapper.find('div').length).toBeGreaterThan(1); }); - it("calls modalAction prop function when a confirm button is clicked", () => { - const confirmButton = wrapper.find("button").at(0); - confirmButton.simulate("click"); + it('calls modalAction prop function when a confirm button is clicked', () => { + const confirmButton = wrapper.find('button').at(0); + confirmButton.simulate('click'); expect(wrapper.instance().props.modalAction).toHaveBeenCalled(); }); -}) \ No newline at end of file +}); diff --git a/client/__tests__/reducers/bookReducer.spec.js b/client/__tests__/reducers/bookReducer.spec.js index 0a8ae55..7f063e3 100644 --- a/client/__tests__/reducers/bookReducer.spec.js +++ b/client/__tests__/reducers/bookReducer.spec.js @@ -4,7 +4,10 @@ import { getBook } from '../../actions/bookActions/viewBook'; import { getBorrowedBooksAction, fetchingBorrowedBooks, - returnBookAction } from '../../actions/bookActions/borrowedBooks'; + returnBookAction, + suggestedBooks, + setBookToRead, +} from '../../actions/bookActions/borrowedBooks'; import { fetchingBooks, getMoreBooks, getBooks, @@ -13,7 +16,12 @@ import { fetchingBooks, getBookCategoriesAction } from '../../actions/bookActions/library'; -import { deleteBookAction } from '../../actions/adminActions/books'; +import { + deleteBookAction, + addCategory, + createBook, + editBookAction, +} from '../../actions/adminActions/books'; let action; @@ -35,6 +43,8 @@ const pagination = { total: 1 }; +const suggestions = [{ cover: 'foo', title: 'bar' }]; + describe('Book Reducer', () => { it('should return initial state for unknown action types', () => { @@ -75,6 +85,27 @@ describe('Book Reducer', () => { expect(newState.borrowedBooks.length).toBe(1); }); + it('should handle actions of type CREATE_BOOK', () => { + const book = { id: 1, title: 'awesome book', authors: 'Flo, Yak' }; + action = createBook(book); + newState = bookReducer(initialState.bookReducer, action); + expect(newState).not.toEqual(initialState.bookReducer); + expect(newState.books) + .toEqual([...initialState.bookReducer.books, book]); + }); + + it('should handle actions of type EDIT_BOOK_INFO', () => { + const edit = { id: 1, title: 'awesome book', authors: 'Flo, Yak' }; + action = editBookAction(edit); + newState = bookReducer(initialState.bookReducer, action); + expect(newState).not.toEqual(initialState.bookReducer); + const filteredBooks = initialState.bookReducer.books.filter(book => ( + book.id !== 1 + )); + expect(newState.books) + .toEqual([...filteredBooks, edit]); + }); + it('should handle actions of type GET_BOOK', () => { const book = { id: 1, title: 'awesome book' }; action = getBook(book); @@ -92,6 +123,13 @@ describe('Book Reducer', () => { expect(JSON.stringify(newState.books)).toEqual(JSON.stringify(books)); }); + it('should handle actions of type GET_SUGGESTED_BOOKS', () => { + action = suggestedBooks(suggestions); + newState = bookReducer(initialState.bookReducer, action); + expect(newState).not.toEqual(initialState.bookReducer); + expect(newState.suggestedBooks).toEqual(suggestions); + }); + it('should handle actions of type GET_MORE_BOOKS', () => { initialState.bookReducer.books = [{ id: 5, title: 'weird' }]; const expected = [...initialState.bookReducer.books, ...books]; @@ -116,6 +154,17 @@ describe('Book Reducer', () => { expect(newState.categories[0].category).toBe('javascript'); }); + + it('should handle actions of type ADD_BOOK_CATEGORY', () => { + action = addCategory({ id: 3, category: 'something new' }); + expect(initialState.bookReducer.categories).toEqual([]); + newState = bookReducer(initialState.bookReducer, action); + expect(newState).not.toEqual(initialState.bookReducer); + expect(newState.categories).toEqual([ + ...initialState.bookReducer.categories, action.category + ]); + }); + it('should handle actions of type SET_LIBRARY_PAGINATION', () => { action = setPagination(pagination); newState = bookReducer(initialState.bookReducer, action); @@ -131,4 +180,12 @@ describe('Book Reducer', () => { expect(newState).not.toEqual(initialState.bookReducer); expect(newState.books.length).toBe(0); }); + + it('should handle actions of type SET_BOOK_TO_READ', () => { + action = setBookToRead('made-up-url'); + expect(initialState.bookReducer.bookToRead).toBe(undefined); + newState = bookReducer(initialState.bookReducer, action); + expect(newState).not.toEqual(initialState.bookReducer); + expect(newState.bookToRead).toBe('made-up-url'); + }); }); diff --git a/client/__tests__/reducers/rootReducer.spec.js b/client/__tests__/reducers/rootReducer.spec.js index 941a775..b1d151a 100644 --- a/client/__tests__/reducers/rootReducer.spec.js +++ b/client/__tests__/reducers/rootReducer.spec.js @@ -1,16 +1,16 @@ -import rootReducer from "../../reducers/rootReducer"; -import initialState from "../../reducers/initialState"; -import { logoutUser } from "../../actions/authActions/logout"; +import rootReducer from '../../reducers/rootReducer'; +import initialState from '../../reducers/initialState'; +import { logoutUser } from '../../actions/authActions/logout'; -describe("Root Reducer", () => { - it("should return initial state for unkwon action types", () => { +describe('Root Reducer', () => { + it('should return initial state for unkwon action types', () => { const action = { type: null }; const newState = rootReducer(initialState, action); expect(newState).toEqual(initialState); }); - it("handles action of type LOGOUT", () => { + it('handles action of type LOGOUT', () => { const action = logoutUser(); const newState = rootReducer(initialState, action); expect(newState).toEqual(initialState); diff --git a/client/__tests__/utils/requestImageUrl.spec.js b/client/__tests__/utils/requestImageUrl.spec.js index 38d7130..1dba3e9 100644 --- a/client/__tests__/utils/requestImageUrl.spec.js +++ b/client/__tests__/utils/requestImageUrl.spec.js @@ -2,17 +2,19 @@ import requestImageUrl from '../../utils/requestImageUrl'; describe('requestImageUrl', () => { it('returns the url if no configuration object is passed', () => { - const baseUrl = "localhost:300"; + const baseUrl = 'localhost:3000'; const result = requestImageUrl(baseUrl); expect(result).toEqual(baseUrl); }); - it('adds width and height configuration the url', () => { - const baseUrl = "localhost:300"; - const result = requestImageUrl(baseUrl, {width: 10, height: 10 }); + it('adds width, height and fill configuration the url', () => { + const baseUrl = 'localhost:3000'; + const result = requestImageUrl(baseUrl, + { width: 10, height: 10, fill: true }); expect(result).not.toEqual(baseUrl); expect(result).toContain('w_10'); - expect(result).toContain("h_10"); + expect(result).toContain('h_10'); + expect(result).toContain('c_fill'); }); }); diff --git a/client/__tests__/utils/saveLocally.spec.js b/client/__tests__/utils/saveLocally.spec.js index 9135081..97a207a 100644 --- a/client/__tests__/utils/saveLocally.spec.js +++ b/client/__tests__/utils/saveLocally.spec.js @@ -1,5 +1,6 @@ import { saveState, loadState } from '../../utils/saveLocally'; import mockLocalStorage from '../__mocks__/mockLocalStorage'; +import store from '../../store'; window.localStorage = mockLocalStorage; @@ -18,12 +19,12 @@ describe('saveState', () => { }); it('fails silently', () => { - localStorage.setItem = () => {throw new Error('something broke')}; + localStorage.setItem = () => { throw new Error('something broke'); }; saveState(state); setTimeout(() => { - expect(localStorage.getItem("state").id).toEqual(false); + expect(localStorage.getItem('state').id).toEqual(false); }, 1000); - }) + }); }); describe('loadState', () => { @@ -35,7 +36,7 @@ describe('loadState', () => { }); it('returns false if state is null', () => { - const nullState = null + const nullState = null; saveState(nullState); setTimeout(() => { expect(loadState()).toEqual(false); diff --git a/client/__tests__/utils/validtation/auth.spec.js b/client/__tests__/utils/validtation/auth.spec.js index 62a4067..848f766 100644 --- a/client/__tests__/utils/validtation/auth.spec.js +++ b/client/__tests__/utils/validtation/auth.spec.js @@ -1,4 +1,4 @@ -import { isEmpty, validateSignUp, validateLogin } +import { isEmpty, validateSignUp, validateLogin, validateForgotPassword } from '../../../utils/validation/auth'; describe('auth', () => { @@ -75,5 +75,13 @@ describe('auth', () => { .toBe('Password is required'); }); }); + + describe('validateForgotPassword', () => { + it('checks if email provided is valid', () => { + const state = { email: '123@i.u' }; + expect(validateForgotPassword(state).errors.email) + .toBe('Invalid email'); + }); + }); }); diff --git a/client/actions/Notify.js b/client/actions/Notify.js new file mode 100644 index 0000000..41f4bee --- /dev/null +++ b/client/actions/Notify.js @@ -0,0 +1,29 @@ +const Materialize = window.Materialize; + +/** + * Materialize.toast() wrapper for success notifications + * + * @param {String} message + * + * @returns {undefined} toasts a success message + */ +const success = (message) => { + Materialize.toast(message, 2500, 'teal darken-4'); +}; + +/** + * Materialize.toast() wrapper for error notifications + * + * @param {String} message + * + * @returns {undefined} toasts an error message + */ +const error = (message) => { + Materialize.toast(message, 2500, 'red darken-4'); +}; + + +export default { + success, + error, +}; diff --git a/client/actions/actionTypes.js b/client/actions/actionTypes.js index 71a1ebd..a62f45d 100644 --- a/client/actions/actionTypes.js +++ b/client/actions/actionTypes.js @@ -1,6 +1,7 @@ import keyMirror from './keyMirror'; /** * array of action types + * * @type {Array} */ const actionList = [ @@ -16,11 +17,15 @@ const actionList = [ 'GET_MORE_BOOKS', 'GET_BORROWED_BOOKS', 'GET_ALL_BORROWED', + 'ADD_BOOK_CATEGORY', + 'ADD_BOOK_CATEGORY_FAILURE', 'GET_BOOK_CATEGORIES', + 'GET_BOOK_SUGGESTIONS', 'BORROW_BOOK', 'RETURN_BOOK', 'CREATE_BOOK', 'READ_BOOK', + 'SET_BOOK_TO_READ', 'EDIT_BOOK_INFO', 'DELETE_BOOK', 'FETCHING_MORE_BOOKS', @@ -42,6 +47,7 @@ const actionList = [ /** * action types object + * * @type {Object} */ const actionTypes = keyMirror(actionList); diff --git a/client/actions/adminActions/books.js b/client/actions/adminActions/books.js index 26239af..ee09bc9 100644 --- a/client/actions/adminActions/books.js +++ b/client/actions/adminActions/books.js @@ -1,40 +1,74 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; +/** + * action creator for editing book + * + * @param {object} book + * + * @return {object} action object + */ +export const editBookAction = book => ({ + type: actionTypes.EDIT_BOOK_INFO, + book, +}); /** * edit book Detail + * * @param {Integer} id book Id * @param {Object} data book data with with to update database - * @return {Object} dispatches an action to the redux store + * + * @return {object} dispatches an action to the redux store */ -export const editBook = (id, data) => () => ( +export const editBook = (id, data) => dispatch => ( axios.put(`${API}/books/${id}`, data) .then((response) => { - notify.success(response.data.message); + Notify.success(response.data.message); + return dispatch(editBookAction(response.data.book)); }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); +/** + * action creator for adding new book + * + * @param {object} book + * + * @return {object} action object + */ +export const createBook = book => ({ + type: actionTypes.CREATE_BOOK, + book, +}); + /** * add new book to database - * @param {Object} data book data - * @return {Object} sends nextwork request + * + * @param {object} data book data + * + * @return {Promise} resolves with success message */ -export const addBook = data => () => ( +export const addBook = data => dispatch => ( axios.post(`${API}/books`, data) - .then(response => notify.success(response.data.message)) - .catch(error => notify.error(error.response.data.message)) + .then((response) => { + Notify.success(response.data.message); + dispatch(createBook(response.data.book)); + }) + .catch(error => reportNetworkError(error)) ); /** * action creator for borrowing books + * * @param {Integer} id book id - * @return {Object} action object + * + * @return {object} action object */ export const deleteBookAction = id => ({ type: actionTypes.DELETE_BOOK, @@ -44,29 +78,60 @@ export const deleteBookAction = id => ({ /** * send request to borrow a book from library + * * @param {integer} bookId book id - * @return {any} dispatches an action to store + * + * @return {Promise} dispatches an action to store */ export const deleteBook = bookId => dispatch => ( axios.delete(`${API}/books/${bookId}`, { id: bookId }) .then((response) => { dispatch(deleteBookAction(bookId)); - notify.success(response.data.message); + Notify.success(response.data.message); return response; }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); +/** + * action creator for adding book category + * + * @param {object} category + * + * @return {object} action object + */ +export const addCategory = category => ({ + type: actionTypes.ADD_BOOK_CATEGORY, + category, +}); + +/** + * action creator for adding book category + * + * @param {string} message + * + * @return {object} action object + */ +export const addCategoryFailure = message => ({ + type: actionTypes.ADD_BOOK_CATEGORY_FAILURE, + message, +}); /** - * het book Detail - * @param {Object} category new book category - * @return {Object} sends nextwork request + * addds a new book category + * + * @param {object} category new book category + * + * @return {Promise} resolves with success message */ -export const addBookCategory = category => () => ( +export const addBookCategory = category => dispatch => ( axios.post(`${API}/books/category`, { category }) .then((response) => { - notify.success(response.data.message); + dispatch(addCategory(response.data.category)); + Notify.success(response.data.message); + }) + .catch((error) => { + dispatch(addCategoryFailure(error.response.data.message)); + reportNetworkError(error); }) - .catch(error => notify.error(error.response.data.message)) ); diff --git a/client/actions/adminActions/notifications.js b/client/actions/adminActions/notifications.js index 8ad3e8c..1e40c34 100644 --- a/client/actions/adminActions/notifications.js +++ b/client/actions/adminActions/notifications.js @@ -2,21 +2,35 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; -import notify from '../notify'; import queryStringFromObject from '../../utils/queryStringFromObject'; +import reportNetworkError from '../reportNetworkError'; +/** + * @param {object} notifications + * + * @return {Object} action object + */ export const adminNotifications = notifications => ({ type: actionTypes.GET_ADMIN_NOTIFICATIONS, notifications, }); +/** + * @param {Array} notifications + * + * @return {Object} action object + */ export const moreAdminNotifications = notifications => ({ type: actionTypes.GET_MORE_ADMIN_NOTIFICATIONS, notifications, }); - +/** + * @param {object} pagination + * + * @return {Object} action object + */ export const setPagination = pagination => ({ type: actionTypes.SET_NOTICATIONS_PAGINATION, pagination, @@ -24,6 +38,7 @@ export const setPagination = pagination => ({ /** * @param {Bool} status + * * @return {Object} action object */ export const fetchingNotifications = status => ({ @@ -34,7 +49,9 @@ export const fetchingNotifications = status => ({ /** * get admin notifications from server + * * @param {object} options + * * @return {Function} dispatches an action creator */ export const fetchNotifications = options => (dispatch) => { @@ -50,6 +67,6 @@ export const fetchNotifications = options => (dispatch) => { }) .catch((error) => { dispatch(fetchingNotifications(false)); - return notify.error(error.response.data.message); + reportNetworkError(error); }); }; diff --git a/client/actions/api.js b/client/actions/api.js index 9501dd2..2db4b03 100644 --- a/client/actions/api.js +++ b/client/actions/api.js @@ -5,6 +5,7 @@ api = process.env.NODE_ENV === ('development' || 'test') ? /** * api url + * * @type {String} */ const API = api; diff --git a/client/actions/authActions/login.js b/client/actions/authActions/login.js index 1a2c2c8..a3a63af 100644 --- a/client/actions/authActions/login.js +++ b/client/actions/authActions/login.js @@ -3,11 +3,15 @@ import actionTypes from '../actionTypes'; import setAuthorizationToken from '../../utils/setAuthorizationToken'; import { authLoading } from './signup'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; /** + * Action creator that sets user data on login + * * @param {Object} user - user data + * * @returns {Object} - Object containing action type and user */ export const loginUser = user => ({ @@ -17,8 +21,11 @@ export const loginUser = user => ({ /** + * Action creator that sets login status + * * @param {Boolean} status - status - * @returns {Object} - Object containing action type and login status + * + * @returns {Object} - Object containing action type and login status */ export const setLoginStatus = status => ({ type: actionTypes.SET_LOGIN_STATUS, @@ -27,8 +34,11 @@ export const setLoginStatus = status => ({ /** + * async action creator for user login + * * @param {object} data - user data - * @returns {any} - dispatches login user action + * + * @returns {Promise} - resolves with user data and authorization token */ export const login = data => (dispatch) => { dispatch(authLoading(true)); @@ -40,11 +50,11 @@ export const login = data => (dispatch) => { dispatch(loginUser(response.data)); dispatch(setLoginStatus(true)); dispatch(authLoading(false)); - notify.success(response.data.message); + Notify.success(response.data.message); return response.data; }) .catch((error) => { - notify.error(error.response.data.message); + reportNetworkError(error); return dispatch(authLoading(false)); }); }; diff --git a/client/actions/authActions/logout.js b/client/actions/authActions/logout.js index 7c6ce3c..111a40f 100644 --- a/client/actions/authActions/logout.js +++ b/client/actions/authActions/logout.js @@ -4,8 +4,9 @@ import actionTypes from '../actionTypes'; export const logoutUser = () => ({ type: actionTypes.LOGOUT }); /** - * @param {Function} dispatch - * @returns {Object} Object containing action type + * action creator for logging a user out + * + * @returns {Function} function that dispatches a logout action */ export default () => (dispatch) => { localStorage.removeItem('token'); diff --git a/client/actions/authActions/requestResetPassword.js b/client/actions/authActions/requestResetPassword.js index 9b549c5..26ea4af 100644 --- a/client/actions/authActions/requestResetPassword.js +++ b/client/actions/authActions/requestResetPassword.js @@ -1,16 +1,20 @@ import axios from 'axios'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; + /** * send request to reset password - * @param {String} email user email - * @returns {Undefined} sends an http request + * + * @param {String} email user email + * + * @returns {Promise} resolves with a success notification */ const requestResetPassword = email => () => ( axios.post(`${API}/users/forgot-password`, { email }) - .then(response => notify.success(response.data.message) - ).catch(error => notify.error(error.response.data.message)) + .then(response => Notify.success(response.data.message)) + .catch(error => reportNetworkError(error)) ); export default requestResetPassword; diff --git a/client/actions/authActions/resetPassword.js b/client/actions/authActions/resetPassword.js index cd45bc2..80411e3 100644 --- a/client/actions/authActions/resetPassword.js +++ b/client/actions/authActions/resetPassword.js @@ -1,17 +1,20 @@ import axios from 'axios'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; /** * send request to reset password +* * @param {String} password new password +* * @param {String} token json web token -* @returns {Undefined} sends an http request +* +* @returns {Promise} resolves with success notification */ -const resetPassword = (password, token) => () => ( - axios.put(`${API}/users/reset-password/${token}`, { password }) - .then(response => notify.success(response.data.message) - ).catch(error => notify.error(error.response.data.message)) -); - +const resetPassword = (password, token) => () => + axios + .put(`${API}/users/reset-password/${token}`, { password }) + .then(response => Notify.success(response.data.message)) + .catch(error => reportNetworkError(error)); export default resetPassword; diff --git a/client/actions/authActions/signup.js b/client/actions/authActions/signup.js index e25bf63..25a6eb5 100644 --- a/client/actions/authActions/signup.js +++ b/client/actions/authActions/signup.js @@ -2,11 +2,15 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; import { setLoginStatus } from './login'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; import setAuthorizationToken from '../../utils/setAuthorizationToken'; /** + * Action creator that sets user in state data on sign up + * * @param {any} user - user + * * @returns {Object} - Object containing action type and user */ export const signUpUser = (user => ({ @@ -15,7 +19,10 @@ export const signUpUser = (user => ({ })); /** + * Action creator that sets loading state + * * @param {Bool} state - loading state + * * @returns {Object} - Object containing action type and loading state */ export const authLoading = (state => ({ @@ -25,7 +32,10 @@ export const authLoading = (state => ({ /** + * async action creator for user sign up + * * @param {any} data - user data + * * @returns {any} - dispatches login user action */ export const signUp = data => (dispatch) => { @@ -38,11 +48,11 @@ export const signUp = data => (dispatch) => { dispatch(signUpUser(response.data)); dispatch(setLoginStatus(true)); dispatch(authLoading(false)); - notify.success(response.data.message); + Notify.success(response.data.message); return response.data; }) .catch((error) => { - notify.error(error.response.data.message); + reportNetworkError(error); return dispatch(authLoading(false)); }); }; diff --git a/client/actions/bookActions/borrowedBooks.js b/client/actions/bookActions/borrowedBooks.js index cd60bb5..2147fad 100644 --- a/client/actions/bookActions/borrowedBooks.js +++ b/client/actions/bookActions/borrowedBooks.js @@ -1,12 +1,14 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; /** * @param {Array} borrowedBooks - array of books borrowed by user - * @returns {Object} - action object + * + * @returns {object} - action object */ export const getBorrowedBooksAction = borrowedBooks => ({ type: actionTypes.GET_BORROWED_BOOKS, @@ -15,7 +17,8 @@ export const getBorrowedBooksAction = borrowedBooks => ({ /** * @param {Bool} status - * @return {Object} action object + * + * @return {object} action object */ export const fetchingBorrowedBooks = status => ({ type: actionTypes.FETCHING_BORROWED_BOOKS, @@ -23,8 +26,9 @@ export const fetchingBorrowedBooks = status => ({ }); /** -* @param {object} id - user id -* @returns {any} - dispatches action with books user has not returned +* @param {Integer} id - user id +* +* @returns {Promise} - dispatches action with books user has not returned */ export const fetchBorrowedBooks = id => (dispatch) => { dispatch(fetchingBorrowedBooks(true)); @@ -35,26 +39,28 @@ export const fetchBorrowedBooks = id => (dispatch) => { }) .catch((error) => { dispatch(fetchingBorrowedBooks(false)); - notify.error(error.response.data.message); + reportNetworkError(error); }); }; /** * @param {object} id - user id -* @returns {any} - dispatches action with all books ever borrowed by user +* +* @returns {Promise} - dispatches action with all books ever borrowed by user */ export const fetchBorrowingHistory = id => dispatch => ( axios.get(`${API}/users/${id}/books`) .then((response) => { dispatch(getBorrowedBooksAction(response.data.books)); }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); /** * @param {integer} id book id - * @return {Object} action object + * + * @return {object} action object */ export const returnBookAction = id => ({ type: actionTypes.RETURN_BOOK, @@ -65,14 +71,55 @@ export const returnBookAction = id => ({ /** * @param {object} userId - user id * @param {object} bookId - book id -* @returns {any} - fetches array of unreturned borrowed books +* +* @returns {Promise} - resolves with an array of unreturned borrowed books */ export const returnBook = (userId, bookId) => dispatch => ( axios.put(`${API}/users/${userId}/books`, { id: bookId }) .then( (response) => { - notify.success(response.data.message); + Notify.success(response.data.message); return dispatch(returnBookAction(bookId)); }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) +); + +/** + * @param {Array} suggestions +* +* @returns {object} action object +*/ +export const suggestedBooks = suggestions => ({ + type: actionTypes.GET_BOOK_SUGGESTIONS, + suggestions +}); + +/** + * fetches book suggestions +* +* @returns {Promise} - resolves with an array ofbook suggestions +*/ +export const getSuggestedBooks = () => dispatch => ( + axios.get(`${API}/books/suggestions`) + .then(response => dispatch(suggestedBooks(response.data.suggestions))) + .catch(error => reportNetworkError(error)) +); + +/** + * @param {string} url +* +* @returns {object} action object +*/ +export const setBookToRead = url => ({ + type: actionTypes.SET_BOOK_TO_READ, + url +}); + +/** + * @param {string} url +* +* @returns {Function} functions that dispatches an action +*/ +export const readBook = url => dispatch => ( + dispatch(setBookToRead(url)) ); diff --git a/client/actions/bookActions/library.js b/client/actions/bookActions/library.js index 9890cc2..e7df426 100644 --- a/client/actions/bookActions/library.js +++ b/client/actions/bookActions/library.js @@ -2,14 +2,17 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; -import notify from '../notify'; +import Notify from '../Notify'; +import reportNetworkError from '../reportNetworkError'; import queryStringFromObject from '../../utils/queryStringFromObject'; /** * action creator for getting books + * * @param {Array} books array of book objects - * @return {Object} action objects + * + * @return {Object} action object */ export const getBooks = books => ({ type: actionTypes.GET_BOOKS, @@ -18,7 +21,9 @@ export const getBooks = books => ({ /** * action creator for getting books + * * @param {Array} books array of book objects + * * @return {Object} action object */ export const getMoreBooks = books => ({ @@ -29,7 +34,9 @@ export const getMoreBooks = books => ({ /** * sets pagination metadata in store + * * @param {Object} paginationData pagination metadata object + * * @return {Object} action object */ export const setPagination = paginationData => ({ @@ -40,6 +47,7 @@ export const setPagination = paginationData => ({ /** * @param {Bool} status + * * @return {Object} action object */ export const fetchingBooks = status => ({ @@ -50,8 +58,10 @@ export const fetchingBooks = status => ({ /** * fetch books in the Library + * * @param {object} options - * @return {Promise} dispatches an action + * + * @return {Promise} resolves with array of books */ export const fetchBooks = options => (dispatch) => { const query = queryStringFromObject(options); @@ -65,19 +75,21 @@ export const fetchBooks = options => (dispatch) => { dispatch(bookAction(response.data.books)); dispatch(setPagination(response.data.metadata)); } else { - notify.error(response.data.message); + Notify.error(response.data.message); } }) .catch((error) => { dispatch(fetchingBooks(false)); - notify.error(error.response.data.message); + reportNetworkError(error); }); }; /** * action creator for borrowing books + * * @param {Integer} id book id + * * @return {Object} action object */ export const borrowBookAction = id => ({ @@ -88,23 +100,25 @@ export const borrowBookAction = id => ({ /** * send request to borrow a book from library + * * @param {integer} userId user id * @param {integer} bookId book id - * @return {any} dispatches an action to store + * + * @return {Promise} resolves with success message */ export const borrowBook = (userId, bookId) => dispatch => ( axios.post(`${API}/users/${userId}/books`, { id: bookId }) .then((response) => { - notify.success(response.data.message); + Notify.success(response.data.message); return dispatch(borrowBookAction(bookId)); }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); /** * @param {Array} categories book categories - * @return {Object} dispatches an action to store + * @return {Object} dispatches an action to store */ export const getBookCategoriesAction = categories => ({ type: actionTypes.GET_BOOK_CATEGORIES, @@ -113,19 +127,20 @@ export const getBookCategoriesAction = categories => ({ /** * get book categories - * @return {any} dispatches an action to the redux store + * + * @return {Promise} resolves with a list of book caategories */ export const getBookCategories = () => dispatch => ( axios.get(`${API}/books/category`) .then(response => ( dispatch(getBookCategoriesAction(response.data.categories)) )) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); export const filterBooksByCategory = categoryId => dispatch => ( axios.get(`${API}/books?categoryId=${categoryId}`) .then(response => dispatch(getBooks(response.data.books))) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); diff --git a/client/actions/bookActions/viewBook.js b/client/actions/bookActions/viewBook.js index e95c580..2c0c703 100644 --- a/client/actions/bookActions/viewBook.js +++ b/client/actions/bookActions/viewBook.js @@ -1,12 +1,13 @@ import axios from 'axios'; import actionTypes from '../actionTypes'; import API from '../api'; -import notify from '../notify'; +import reportNetworkError from '../reportNetworkError'; /** - * @param {Object} book - book - * @returns {Object} - Object containing action type and book + * @param {object} book - book + * + * @returns {object} - Object containing action type and book */ export const getBook = book => ({ type: actionTypes.GET_BOOK, @@ -15,11 +16,13 @@ export const getBook = book => ({ /** * get book Detail - * @param {Integer} id book Id - * @return {any} dispatches an action to the redux store + * + * @param {Integer} id book Id + * + * @return {Promise} resolves with book information */ export const viewBookDetails = id => dispatch => ( axios.get(`${API}/books/${id}`) .then(response => dispatch(getBook(response.data.book))) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); diff --git a/client/actions/history.js b/client/actions/history.js index 6f3e7ec..62c9025 100644 --- a/client/actions/history.js +++ b/client/actions/history.js @@ -2,13 +2,15 @@ import axios from 'axios'; import actionTypes from '../actions/actionTypes'; import API from './api'; -import notify from './notify'; +import reportNetworkError from './reportNetworkError'; import queryStringFromObject from '../utils/queryStringFromObject'; /** * Action creator for getting list of all books a userr has ever borrowed + * * @param {Array} books array of books + * * @return {Object} redux action with type and books properties */ export const fetchHistoryAction = books => ({ @@ -27,8 +29,10 @@ export const fetchingHistory = status => ({ /** * fetch list of all books a user has ever borrowed + * * @param {Integer} id user id - * @return {Thunk} a function that retuns a function (action creator) + * + * @return {Promise} resolves with borrowing history */ export const fetchHistory = id => (dispatch) => { dispatch(fetchingHistory(true)); @@ -38,15 +42,17 @@ export const fetchHistory = id => (dispatch) => { dispatch(fetchHistoryAction(response.data.books)); }).catch((error) => { dispatch(fetchingHistory(false)); - return notify.error(error.response.data.message); + return reportNetworkError(error); }); }; /** * Action creator for getting full transactions history + * * @param {array} transactions array of transacitons - * @return {Thunk} function that returns an action creator + * + * @return {Promise} function that returns an action creator */ export const getTransactionHistory = transactions => ({ type: actionTypes.GET_TRANSACTION_HISTORY, @@ -65,7 +71,9 @@ export const fetchingTransactions = status => ({ /** * sets pagination metadata in store + * * @param {Object} pagination pagination metadata object + * * @return {Object} action object */ export const setTransactionPagination = pagination => ({ @@ -73,6 +81,14 @@ export const setTransactionPagination = pagination => ({ pagination }); +/** + * fetch list of all books a user has ever borrowed + * + * @param {pbject} options + * @param {Integer} id user id + * + * @return {Promise} resolves with transaction history + */ export const fetchTransactionHistory = (options, id) => (dispatch) => { dispatch(fetchingTransactions(true)); const query = queryStringFromObject(options); @@ -86,6 +102,6 @@ export const fetchTransactionHistory = (options, id) => (dispatch) => { }) .catch((error) => { dispatch(fetchingTransactions(false)); - notify.error(error.response.data.message); + reportNetworkError(error); }); }; diff --git a/client/actions/keyMirror.js b/client/actions/keyMirror.js index 645dc5d..c7d4e93 100644 --- a/client/actions/keyMirror.js +++ b/client/actions/keyMirror.js @@ -1,8 +1,11 @@ /** * converts an array to Object with array items as keys and values + * * @example ['books'] becomes { books: 'books' } + * * @param {Array|Object} keys array of keys or object with required keys - * @return {Object} Object with keys and values mirrored + * + * @returns {Object} Object with keys and values mirrored */ const keyMirror = (keys) => { keys = Array.isArray(keys) ? keys : Object.keys(keys); diff --git a/client/actions/notify.js b/client/actions/notify.js deleted file mode 100644 index eafe876..0000000 --- a/client/actions/notify.js +++ /dev/null @@ -1,15 +0,0 @@ -const Materialize = window.Materialize; - -const success = (message) => { - Materialize.toast(message, 2500, 'teal darken-4'); -}; - -const error = (message) => { - Materialize.toast(message, 2500, 'red darken-4'); -}; - - -export default { - success, - error, -}; diff --git a/client/actions/reportNetworkError.js b/client/actions/reportNetworkError.js new file mode 100644 index 0000000..933af04 --- /dev/null +++ b/client/actions/reportNetworkError.js @@ -0,0 +1,7 @@ +import Notify from './Notify'; + +const reportNetworkError = error => (error.response ? + Notify.error(error.response.data.message) : + Notify.error(`${error.message}. It appears you're offline`)); + +export default reportNetworkError; diff --git a/client/actions/updateProfile.js b/client/actions/updateProfile.js index c3f15ca..168d97e 100644 --- a/client/actions/updateProfile.js +++ b/client/actions/updateProfile.js @@ -2,12 +2,16 @@ import axios from 'axios'; import API from './api'; import setAuthorizationToken from '../utils/setAuthorizationToken'; import { loginUser } from './authActions/login'; -import notify from './notify'; +import Notify from './Notify'; +import reportNetworkError from './reportNetworkError'; + /** * action creator for updating user information + * * @param {Object} profile profile data - * @return {Thunk} function that dispatches an action + * + * @return {Promise} resolves with updated user data */ const updateProfile = profile => dispatch => ( axios.put(`${API}/users`, profile) @@ -15,10 +19,10 @@ const updateProfile = profile => dispatch => ( const token = response.data.token; localStorage.setItem('token', token); setAuthorizationToken(token); - notify.success(response.data.message); + Notify.success(response.data.message); return dispatch(loginUser(response.data)); }) - .catch(error => notify.error(error.response.data.message)) + .catch(error => reportNetworkError(error)) ); export default updateProfile; diff --git a/client/actions/uploadFile.js b/client/actions/uploadFile.js index dbb04e4..e27d891 100644 --- a/client/actions/uploadFile.js +++ b/client/actions/uploadFile.js @@ -1,11 +1,21 @@ import request from 'superagent'; - +/** + * utility function for uploading files + * + * @param {object} file DOM file object + * + * @returns {Promise} resolves with file metadata + */ export default file => ( () => ( - request - .post(CLOUDINARY_API_BASE) - .field('upload_preset', CLOUDINARY_UPLOAD_PRESET) - .field('file', file) - ) + new Promise((resolve, reject) => { + request + .post(CLOUDINARY_API_BASE) + .field('upload_preset', CLOUDINARY_UPLOAD_PRESET) + .field('file', file) + .end((error, response) => ( + error ? reject(error) : resolve(response) + )); + })) ); diff --git a/client/components/Admin/CategoryForm.jsx b/client/components/Admin/AddCategoryForm.jsx similarity index 99% rename from client/components/Admin/CategoryForm.jsx rename to client/components/Admin/AddCategoryForm.jsx index d1669ad..ad8b2fd 100644 --- a/client/components/Admin/CategoryForm.jsx +++ b/client/components/Admin/AddCategoryForm.jsx @@ -5,7 +5,9 @@ import { Button, Input } from 'react-materialize'; /** * displays form for adding new book category + * * @param {Object} props + * * @returns {JSX} JSX representation of component */ const AddCategoryForm = props => ( diff --git a/client/components/Admin/BookForm.jsx b/client/components/Admin/BookForm.jsx index 94e3d72..0e3b166 100644 --- a/client/components/Admin/BookForm.jsx +++ b/client/components/Admin/BookForm.jsx @@ -3,10 +3,14 @@ import PropTypes from 'prop-types'; import Categories from '../common/Categories'; import Loading from '../common/Loading'; +import requestImageUrl from '../../utils/requestImageUrl'; + /** * for for adding or editing books + * * @param {object} props + * * @returns {JSX} JSX representation of component */ const BookForm = (props) => { @@ -15,6 +19,8 @@ const BookForm = (props) => { text='Select Book Category' categories={props.categories} onChange={props.onSelectCategory} + indexValue={1} + indexText="default category" /> : null; return (
@@ -33,6 +39,9 @@ const BookForm = (props) => { placeholder="Book Title" onChange={event => props.onChange(event)} /> + + {props.errors.title} +
{ placeholder="Authors (comma separated for multiple)" onChange={event => props.onChange(event)} /> + + {props.errors.authors} +