diff --git a/app/components/invoices/__tests__/Invoice.spec.js b/app/components/invoices/__tests__/Invoice.spec.js index 9c68b1cd..d797b4bd 100644 --- a/app/components/invoices/__tests__/Invoice.spec.js +++ b/app/components/invoices/__tests__/Invoice.spec.js @@ -32,6 +32,7 @@ const invoice = { }, }; +const editInvoice = jest.fn(); const deleteInvoice = jest.fn(); const setInvoiceStatus = jest.fn(); const dateFormat = 'MM/DD/YY'; @@ -43,6 +44,7 @@ describe('Renders correctly to the DOM', () => { wrapper = shallow( { const mountWrapper = mount( { const paidInvoiceWapper = shallow( { const cancelledInvoiceWapper = shallow( { const refundedInvoiceWapper = shallow( { .create( { wrapper = shallow( { deleteBtn = wrapper.find(Button).first(); }); + // TODO + it('handle edit action correctly', () => { + editBtn.simulate('click'); + expect(editInvoice).toHaveBeenCalled(); + expect(editInvoice).toHaveBeenCalledWith(invoice); + }); + it('handle delete action correctly', () => { deleteBtn.simulate('click'); expect(deleteInvoice).toHaveBeenCalled(); diff --git a/app/helpers/__mocks__/pouchDB.js b/app/helpers/__mocks__/pouchDB.js index a9eb5307..bfac99e1 100644 --- a/app/helpers/__mocks__/pouchDB.js +++ b/app/helpers/__mocks__/pouchDB.js @@ -52,6 +52,16 @@ const saveDoc = jest.fn( }) ); +const updateDoc = jest.fn( + (dbName, doc) => + new Promise((resolve, reject) => { + !dbName && reject(new Error('No database found!')); + !doc && reject(new Error('No doc found!')); + dbName === 'contacts' && resolve([...mockData.contactsRecords]); + dbName === 'invoices' && resolve([...mockData.invoicesRecords]); + }) +); + const deleteDoc = jest.fn( (dbName, docId) => new Promise((resolve, reject) => { @@ -70,9 +80,11 @@ const deleteDoc = jest.fn( }) ); + module.exports = { getAllDocs, saveDoc, + updateDoc, deleteDoc, mockData, }; diff --git a/app/middlewares/InvoicesMW.jsx b/app/middlewares/InvoicesMW.jsx index ac7f2db0..81cc149d 100644 --- a/app/middlewares/InvoicesMW.jsx +++ b/app/middlewares/InvoicesMW.jsx @@ -113,7 +113,7 @@ const InvoicesMW = ({ dispatch }) => next => action => { type: ACTION_TYPES.UI_NOTIFICATION_NEW, payload: { type: 'success', - message: 'Updated Successfully', + message: 'Invoice Updated Successfully', }, }); }) diff --git a/app/middlewares/__tests__/FormMW.spec.js b/app/middlewares/__tests__/FormMW.spec.js index 2a1b60dd..cd266fdd 100644 --- a/app/middlewares/__tests__/FormMW.spec.js +++ b/app/middlewares/__tests__/FormMW.spec.js @@ -18,6 +18,11 @@ describe('Form Middleware', () => { form: { validation: true, recipient: { newRecipient: true }, + settings: { + editMode: { + active: false, + } + } }, })); const middleware = FormMW({ dispatch, getState })(next); @@ -40,6 +45,11 @@ describe('Form Middleware', () => { form: { validation: true, recipient: { newRecipient: false }, + settings: { + editMode: { + active: false, + } + } }, })); const middleware = FormMW({ dispatch, getState })(next); @@ -55,6 +65,60 @@ describe('Form Middleware', () => { // No Calling Next expect(next.mock.calls.length).toBe(0); }); + + it('should update Invocie, save Contact, Clear the Form and Switch Tab', () => { + // Setup + getState = jest.fn(() => ({ + form: { + validation: true, + recipient: { newRecipient: true }, + settings: { + editMode: { + active: true, + data: { _id: 'invoice-uuid' } + } + } + }, + })); + const middleware = FormMW({ dispatch, getState })(next); + const action = Actions.saveFormData(); + // Action + middleware(action); + // Expect + expect(getState.mock.calls.length).toBe(1); + // Update the Invoice, Save new contact, Clear the Form & Change Tab + expect(dispatch.mock.calls.length).toBe(4); + // No Calling Next + expect(next.mock.calls.length).toBe(0); + }); + + it('should update Invocie, NOT add new Contact, Clear the Form and Switch Tab', () => { + // Setup + getState = jest.fn(() => ({ + form: { + validation: true, + recipient: { newRecipient: false }, + settings: { + editMode: { + active: true, + data: { _id: 'invoice-uuid' } + } + } + }, + })); + const middleware = FormMW({ dispatch, getState })(next); + + // Action + const action = Actions.saveFormData(); + middleware(action); + + // Expect + expect(getState.mock.calls.length).toBe(1); + // Update the Invoice, Clear the Form and Switch Tab + expect(dispatch.mock.calls.length).toBe(3); + // No Calling Next + expect(next.mock.calls.length).toBe(0); + }); }); it('should NOT pass validation', () => { diff --git a/app/middlewares/__tests__/InvoicesMW.spec.js b/app/middlewares/__tests__/InvoicesMW.spec.js index e8beb65e..ce84b24f 100644 --- a/app/middlewares/__tests__/InvoicesMW.spec.js +++ b/app/middlewares/__tests__/InvoicesMW.spec.js @@ -7,6 +7,7 @@ import uuidv4 from 'uuid/v4'; // Mock Functions const { getAllDocs, + updateDoc, saveDoc, deleteDoc, mockData, @@ -278,6 +279,122 @@ describe('Invoices Middleware', () => { }); }); + describe('should handle INVOICE_EDIT action', () => { + it('should call next and dispatch change Tab action', () => { + // Setup + const currentInvoice = { + recipient: { + fullname: faker.name.findName(), + email: faker.internet.email(), + }, + currency: { + code: 'USD', + symbol: '$', + }, + rows: [ + { + id: uuidv4(), + description: faker.commerce.productName(), + price: faker.commerce.price(), + quantity: faker.random.number(10), + }, + ], + }; + // Execute + const action = Actions.editInvoice(currentInvoice); + middleware(action); + // Call next + expect(next.mock.calls.length).toBe(1); + expect(next).toHaveBeenCalledWith(action); + // Dispatch change Tab action + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_TAB_CHANGE, + payload: 'form' + }); + }); + }) + + // TODO + describe('should handle INVOICE_UDPATE action', () => { + let currentInvoice; + beforeEach(() => { + currentInvoice = { + recipient: { + fullname: faker.name.findName(), + email: faker.internet.email(), + }, + currency: { + code: 'USD', + symbol: '$', + }, + rows: [ + { + id: uuidv4(), + description: faker.commerce.productName(), + price: faker.commerce.price(), + quantity: faker.random.number(10), + }, + ], + }; + }); + + it('should update the invoice', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => + updateDoc('invoices', currentInvoice).then(data => { + expect(data).toEqual(mockData.invoicesRecords); + }) + ); + }); + + it('should call next and dispatch notification', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => + updateDoc('invoices', currentInvoice).then(data => { + // Call next after the promised is returned + expect(next.mock.calls.length).toBe(1); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.INVOICE_UPDATE, + payload: mockData.invoicesRecords, + }); + // Dispatch success notification + expect(dispatch.mock.calls.length).toBe(1); + expect(dispatch).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'success', + message: 'Invoice Updated Successfully', + }, + }); + }) + ); + }); + + it('should handle syntax error correctly', () => { + middleware(Actions.updateInvoice(currentInvoice)).then(() => { + const dbError = new Error('No database found!'); + const docError = new Error('No doc found!'); + expect(updateDoc()).rejects.toEqual(dbError); + expect(updateDoc('invoices')).rejects.toEqual(docError); + }); + }); + + it('should handle unkown error correctly', () => { + const expectedError = new Error('Something Broken!'); + updateDoc.mockImplementationOnce(() => Promise.reject(expectedError)); + middleware(Actions.updateInvoice(currentInvoice)).then(() => { + expect(next).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith({ + type: ACTION_TYPES.UI_NOTIFICATION_NEW, + payload: { + type: 'warning', + message: expectedError.message, + }, + }); + }); + }); + }) + + describe('should handle INVOICE_DELETE action', () => { it('should remove record from DB correctly', () => { const invoiceID = 'jon-invoice'; diff --git a/app/reducers/FormReducer.jsx b/app/reducers/FormReducer.jsx index 0015cb6e..c41d066a 100644 --- a/app/reducers/FormReducer.jsx +++ b/app/reducers/FormReducer.jsx @@ -25,7 +25,7 @@ const initialState = { settings: { open: false, editMode: { - active: false + active: false, }, required_fields: invoiceSettings.required_fields, }, @@ -123,14 +123,20 @@ const FormReducer = handleActions( }), rows, currency, - dueDate: dueDate !== undefined ? Object.assign({}, state.dueDate, { - selectedDate: dueDate - }) : state.dueDate, + dueDate: + dueDate !== undefined + ? Object.assign({}, state.dueDate, { + selectedDate: dueDate, + }) + : state.dueDate, discount: discount !== undefined ? discount : state.discount, tax: tax !== undefined ? tax : state.tax, - note: note !== undefined ? Object.assign({}, state.note, { - content: note - }) : state.note, + note: + note !== undefined + ? Object.assign({}, state.note, { + content: note, + }) + : state.note, // Update settings settings: Object.assign({}, state.settings, { editMode: { @@ -143,7 +149,7 @@ const FormReducer = handleActions( dueDate: dueDate !== undefined, discount: discount !== undefined, note: note !== undefined, - }) + }), }), }); }, @@ -169,7 +175,7 @@ const FormReducer = handleActions( settings: Object.assign({}, state.settings, { open: false, editMode: { - active: false + active: false, }, required_fields: state.savedSettings.required_fields, }), diff --git a/app/reducers/__tests__/FormReducer.spec.js b/app/reducers/__tests__/FormReducer.spec.js index 9021576a..9bea5a22 100644 --- a/app/reducers/__tests__/FormReducer.spec.js +++ b/app/reducers/__tests__/FormReducer.spec.js @@ -311,6 +311,211 @@ describe('Form Reducer should handle update', () => { }); }); +describe('Form Reducer should handle Invoice Edit', () => { + let currentState, invoiceData, newState; + beforeEach(() => { + currentState = { + recipient: { + newRecipient: true, + select: {}, + new: {}, + }, + rows: [], + dueDate: {}, + discount: {}, + note: {}, + currency: { + code: 'USD', + decimal_digits: 2, + name: 'US Dollar', + name_plural: 'US dollars', + rounding: 0, + symbol: '$', + symbol_native: '$', + }, + tax: { + amount: 10, + method: 'default', + tin: '123-456-789', + }, + settings: { + open: false, + editMode: { + active: false, + }, + required_fields: { + dueDate: false, + currency: false, + discount: false, + tax: false, + note: false, + }, + }, + savedSettings: { + currency: 'VND', + }, + }; + invoiceData = { + recipient: { + fullname: 'Jon Snow', + company: 'HBO', + email: 'jon@snow.got', + phone: '000000000', + }, + rows: [ + { + id: '16bf1a07-71e6-4be4-8a18-d89a715bd191', + description: 'iPhone X', + price: 999, + quantity: 1, + subtotal: 999 + }, + ], + currency: { + code: 'USD', + decimal_digits: 2, + name: 'US Dollar', + name_plural: 'US dollars', + rounding: 0, + symbol: '$', + symbol_native: '$', + }, + tax: { + amount: 5, + method: 'reverse-charge', + tin: '555-444-333', + }, + dueDate: { + date: 1, + hours: 12, + milliseconds: 0, + minutes: 0, + months: 1, + seconds: 0, + years: 2018, + }, + discount: { + amount: 5, + type: 'percentage' + }, + note: 'Thank you!', + }; + newState = FormReducer(currentState, { + type: ACTION_TYPES.INVOICE_EDIT, + payload: invoiceData, + }); + }); + + it('change editMode to true and add editData', () => { + expect(newState.settings.editMode.active).toEqual(true); + expect(newState.settings.editMode.data).toEqual(invoiceData); + }); + + it('should populate field data', () => { + // Recipient + expect(newState.recipient.newRecipient).toEqual(false); + expect(newState.recipient.select).toEqual(invoiceData.recipient); + // Rows + expect(newState.rows.length).toEqual(1); + expect(newState.rows).toEqual(invoiceData.rows); + // Tax + expect(newState.tax).toEqual(invoiceData.tax); + // Currency + expect(newState.currency).toEqual(invoiceData.currency); + // Note + expect(newState.note.content).toEqual(invoiceData.note); + // Discount + expect(newState.discount).toEqual(invoiceData.discount); + // DueDate + expect(newState.dueDate.selectedDate).toEqual(invoiceData.dueDate); + }); + + it('toggle optional field if necessary', () => { + const { required_fields } = newState.settings; + expect(required_fields.tax).toEqual(invoiceData.tax !== undefined); + expect(required_fields.dueDate).toEqual(invoiceData.dueDate !== undefined); + expect(required_fields.discount).toEqual(invoiceData.discount !== undefined); + expect(required_fields.note).toEqual(invoiceData.note !== undefined); + }); + + it('should only show currency field if it is different than default', () => { + const currentCurrencyCode = newState.currency.code; + const savedCurrencyCode = newState.savedSettings.currency; + expect(newState.settings.required_fields.currency).toEqual( + currentCurrencyCode !== savedCurrencyCode + ); + }); +}); + +describe('Form Reducer should handle update Settings', () => { + let currentState, newState; + beforeEach(() => { + currentState = { + recipient: { + newRecipient: true, + select: {}, + new: {}, + }, + rows: [], + savedSettings: { + tax: { + amount: 10, + method: 'default', + tin: '123-456-789', + }, + currency: 'USD', + required_fields: { + dueDate: false, + currency: false, + discount: false, + tax: false, + note: false, + } + }, + }; + newState = FormReducer(currentState, { + type: ACTION_TYPES.SAVED_FORM_SETTING_UPDATE, + payload: { + tax: { + amount: 5, + method: 'reverse', + tin: '111-111-111', + }, + currency: 'VND', + required_fields: { + dueDate: true, + currency: true, + discount: true, + tax: true, + note: true, + } + }, + }); + }); + + it('should save default tax value', () => { + expect(newState.savedSettings.tax).toEqual({ + amount: 5, + method: 'reverse', + tin: '111-111-111', + }); + }); + + it('should save default currency value', () => { + expect(newState.savedSettings.currency).toEqual('VND'); + }); + + it('should save default visibility value', () => { + expect(newState.savedSettings.required_fields).toEqual({ + dueDate: true, + currency: true, + discount: true, + tax: true, + note: true, + }); + }); +}); + // Test Selectors const state = { form: {