diff --git a/.env_test b/.env_test new file mode 100644 index 0000000..851861a --- /dev/null +++ b/.env_test @@ -0,0 +1 @@ +CI="true" diff --git a/.example.env b/.example.env index 8c10d90..de52b4a 100644 --- a/.example.env +++ b/.example.env @@ -1,6 +1,5 @@ -REACT_APP_SERVICE_ID=7891245607 -REACT_APP_GONEBUSY_TOKEN="Token af9094c6d46658e60cde12e34ad26979" -REACT_APP_API_HOST="http://sandbox.gonebusy.com" -REACT_APP_API_PATH="/api/v1" -REACT_APP_IS_PROXIED="true" -REACT_APP_PROXY_HOST="http://localhost:3000" +GONEBUSY_TOKEN="Token af9094c6d46658e60cde12e34ad26979" +GONEBUSY_API_HOST="http://sandbox.gonebusy.com" +GONEBUSY_API_PATH="/api/v1" +GONEBUSY_IS_PROXIED="true" +GONEBUSY_PROXY_HOST="http://localhost:3000" diff --git a/.example.env_test b/.example.env_test new file mode 100644 index 0000000..851861a --- /dev/null +++ b/.example.env_test @@ -0,0 +1 @@ +CI="true" diff --git a/config/env.js b/config/env.js index 5d0ab7b..383b9fe 100644 --- a/config/env.js +++ b/config/env.js @@ -2,19 +2,22 @@ // injected into the application via DefinePlugin in Webpack configuration. var REACT_APP = /^REACT_APP_/i; +var clientParams = require('./gonebusy_env').client; function getClientEnvironment(publicUrl) { + var envData = Object.assign({}, process.env, clientParams); + var processEnv = Object - .keys(process.env) + .keys(envData) .filter(key => REACT_APP.test(key)) .reduce((env, key) => { - env[key] = JSON.stringify(process.env[key]); + env[key] = JSON.stringify(envData[key]); return env; }, { // Useful for determining whether we’re running in production mode. // Most importantly, it switches React into the correct mode. 'NODE_ENV': JSON.stringify( - process.env.NODE_ENV || 'development' + envData.NODE_ENV || 'development' ), // Useful for resolving the correct path to static assets in `public`. // For example, . diff --git a/config/gonebusy_env.js b/config/gonebusy_env.js index b467b7e..113b824 100644 --- a/config/gonebusy_env.js +++ b/config/gonebusy_env.js @@ -1,26 +1,28 @@ const url = require('url'); const env = process.env; -const reactAppServiceId = env['REACT_APP_SERVICE_ID']; -const reactAppGonebusyToken = env['REACT_APP_GONEBUSY_TOKEN']; -const gonebusyApiHost = env['REACT_APP_API_HOST']; -const gonebusyApiPath = env['REACT_APP_API_PATH']; -const gonebusyIsProxied = env['REACT_APP_IS_PROXIED']; -const gonebusyProxyHost = env['REACT_APP_PROXY_HOST']; -const is_proxied = !!(gonebusyIsProxied && JSON.parse(gonebusyIsProxied)); +const envToken = env['GONEBUSY_TOKEN']; +const envApiHost = env['GONEBUSY_API_HOST']; +const envApiPath = env['GONEBUSY_API_PATH']; +const envIsProxied = env['GONEBUSY_IS_PROXIED']; +const envProxyHost = env['GONEBUSY_PROXY_HOST']; -const clientApiEndpoint = url.resolve((is_proxied ? gonebusyProxyHost : gonebusyApiHost) || '', gonebusyApiPath); -const clientToken = is_proxied ? 'none' : reactAppGonebusyToken; -const middlewareProxyHost = is_proxied ? gonebusyApiHost : undefined; -const middlewareToken = is_proxied ? reactAppGonebusyToken : undefined; +const is_proxied = !!(envIsProxied && JSON.parse(envIsProxied)); -console.log("to change the way we process .env so that it won't appear in plain JS", is_proxied); +const clientApiEndpoint = url.resolve((is_proxied ? envProxyHost : envApiHost) || '', envApiPath); +const clientToken = is_proxied ? 'none' : envToken; + +const middlewareProxyHost = is_proxied ? envApiHost : undefined; +const middlewareToken = is_proxied ? envToken : undefined; module.exports = { - service_id: reactAppServiceId, - clientApiEndpoint, - clientToken, - middlewareProxyHost, - middlewarePath: gonebusyApiPath, - middlewareToken, + client: { + REACT_APP_API_ENDPOINT: clientApiEndpoint, + REACT_APP_TOKEN: clientToken + }, + middleware: { + proxy: middlewareProxyHost, + path: envApiPath, + token: middlewareToken + } }; diff --git a/package.json b/package.json index b9b09b4..2755c98 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "path-exists": "2.1.0", "postcss-loader": "1.0.0", "promise": "7.1.1", + "react-addons-test-utils": "^15.4.2", "react-dev-utils": "^0.4.2", + "react-test-renderer": "^15.4.2", "recursive-readdir": "2.1.0", "strip-ansi": "3.0.1", "style-loader": "0.13.1", diff --git a/scripts/start.js b/scripts/start.js index 1336e60..ecb236b 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -156,10 +156,10 @@ function addMiddleware(devServer) { // `proxy` lets you to specify a fallback server during development. // Every unrecognized request will be forwarded to it. - const gonebusy_env = require('../config/gonebusy_env'); - const proxy = gonebusy_env['middlewareProxyHost']; - const token = gonebusy_env['middlewareToken']; - const middlewarePath = gonebusy_env['middlewarePath']; + var envParams = require('../config/gonebusy_env').middleware; + var proxy = envParams.proxy; + var token = envParams.token; + var apiPath = envParams.path; devServer.use(historyApiFallback({ // Paths with dots should still use the history fallback. @@ -192,7 +192,7 @@ function addMiddleware(devServer) { // Tip: use https://jex.im/regulex/ to visualize the regex // var mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/; // var mayProxy = /^\/api.*$/; - var mayProxy = new RegExp('^' + middlewarePath + '.*$'); + var mayProxy = new RegExp('^' + apiPath + '.*$'); // Pass the scope regex both to Express and to the middleware for proxying // of both HTTP and WebSockets to work without false positives. @@ -204,13 +204,13 @@ function addMiddleware(devServer) { proxyReq.setHeader('authorization', token); console.log(`requested: [${req.method}] ${req.url}`); console.log('query:', req.query); - console.log('-\n'); + console.log('-'); }, onProxyRes: (proxyRes, req, res) => { proxyRes.on('data', (chunk) => { console.log(`response for: [${req.method}] ${req.url}`); console.log((new StringDecoder('utf8')).write(chunk)); - console.log('-\n'); + console.log('-'); }); }, // pathRewrite: { diff --git a/scripts/test.js b/scripts/test.js index c4dc347..6a78928 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -5,13 +5,19 @@ process.env.PUBLIC_URL = ''; // if this file is missing. dotenv will never modify any environment variables // that have already been set. // https://github.com/motdotla/dotenv -require('dotenv').config({silent: true}); +require('dotenv').config({ + silent: true, + path: './.env_test' +}); const jest = require('jest'); const argv = process.argv.slice(2); +// console.log(process.env); + // Watch unless on CI or in coverage mode if (!process.env.CI && argv.indexOf('--coverage') < 0) { + console.log('got inside'); argv.push('--watch'); } diff --git a/src/__tests__sample/Bookie.js b/src/__tests__sample/Bookie.js new file mode 100644 index 0000000..23a4522 --- /dev/null +++ b/src/__tests__sample/Bookie.js @@ -0,0 +1,19 @@ +jest.dontMock('../Bookie.jsx'); + +describe('Bookie', function () { + var React = require('react'); + var ReactDOM = require('react-dom'); + var TestUtils = require('react-addons-test-utils'); + + var Bookie; + + beforeEach(function () { + Bookie = require('../Bookie').default; + }); + + it('should exists', function () { + // Render into document + var bookie = TestUtils.renderIntoDocument(); + expect(TestUtils.isCompositeComponent(bookie)).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/components/App.test.jsx b/src/components/App.test.jsx index b84af98..948f468 100644 --- a/src/components/App.test.jsx +++ b/src/components/App.test.jsx @@ -2,6 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +jest.mock('../lib/BusyAdapter'); +jest.mock('../lib/Scheduler'); + it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); diff --git a/src/components/Bookie.jsx b/src/components/Bookie.jsx index 6640abf..023e9aa 100644 --- a/src/components/Bookie.jsx +++ b/src/components/Bookie.jsx @@ -75,7 +75,7 @@ class Bookie extends Component { pickerUpdater.adjust(); if (setLoading) this.setParentLoading(pickerUpdater.state().loading); - console.log('applying diff', diff, pickerUpdater.diff()); + // console.log('applying diff', diff, pickerUpdater.diff()); this.setState(pickerUpdater.diff(), () => { this.pullMissingData(); }); } } diff --git a/src/components/Bookie.test.data.js b/src/components/Bookie.test.data.js new file mode 100644 index 0000000..a4e4ede --- /dev/null +++ b/src/components/Bookie.test.data.js @@ -0,0 +1,176 @@ +const savedState = { + "initialized": true, + "loading": false, + "daysFrameStart": "2017-01-01", + "dayPicked": "2017-01-01", + "hourPicked": 18, + "minutesIdxPicked": 1, + "daysToFetch": [], + "dayData": { + "2017-01-01": { + "presentSlots": { + "9": [ + 0, + 15, + 30, + 45 + ], + "10": [ + 0, + 15, + 30, + 45 + ], + "11": [ + 0, + 15, + 30, + 45 + ], + "12": [ + 0, + 15, + 30, + 45 + ], + "13": [ + 0, + 15, + 30, + 45 + ], + "14": [ + 0, + 15, + 30, + 45 + ], + "15": [ + 0, + 15, + 30, + 45 + ], + "16": [ + 0, + 15, + 30, + 45 + ], + "18": [ + 0, + 15, + 30, + 45 + ], + "19": [ + 0, + 15, + 30, + 45 + ] + }, + "presentHours": [ + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 18, + 19 + ] + }, + "2017-01-02": { + "presentSlots": { + "9": [ + 0, + 15, + 30, + 45 + ], + "10": [ + 0, + 15, + 30, + 45 + ], + "11": [ + 0, + 15, + 30, + 45 + ], + "12": [ + 0, + 15, + 30, + 45 + ], + "13": [ + 0, + 15, + 30, + 45 + ], + "14": [ + 0, + 15, + 30, + 45 + ], + "15": [ + 0, + 15, + 30, + 45 + ], + "16": [ + 0, + 15, + 30, + 45 + ], + "17": [ + 0, + 15, + 30, + 45 + ], + "18": [ + 0, + 15, + 30, + 45 + ], + "19": [ + 0, + 15, + 30, + 45 + ] + }, + "presentHours": [ + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19 + ] + } + }, + "startVal": "2017-01-01T18:15:00+02:00", + "endVal": "2017-01-01T19:00:00+02:00", +}; + +module.exports = { + savedState +}; diff --git a/src/components/Bookie.test.jsx b/src/components/Bookie.test.jsx new file mode 100644 index 0000000..f3d7e5d --- /dev/null +++ b/src/components/Bookie.test.jsx @@ -0,0 +1,64 @@ +const React = require('react'); +const ReactDOM = require('react-dom'); +import ReactDOMServer from 'react-dom/server'; + +const TestUtils = require('react-addons-test-utils'); +const Bookie = require('./Bookie').default; +const renderer = require('react-test-renderer'); + +jest.mock('../lib/Scheduler'); +jest.mock('../lib/BusyAdapter'); + +it('should exists', () => { + const bookie = TestUtils.renderIntoDocument(); + expect(TestUtils.isCompositeComponent(bookie)).toBeTruthy(); +}); + +// it('markup matches snapshot', () => { +// const tree = renderer.create().toJSON(); +// expect(tree).toMatchSnapshot(); +// }); + +describe('enabling bookie button', () => { + // there's a gap on 2017-01-01 at 17:00-18:00 + const savedState = require('./Bookie.test.data').savedState; + + const createBookieWithState = function(state) { + const result = TestUtils.renderIntoDocument(); + result.setState(state); + result.negotiateStateDiff({}, true); + return result; + } + + it('is enabled for valid range', () => { + const bookieComponent = createBookieWithState(savedState); + + // ensure booking is enabled in state + expect(bookieComponent.state.bookingAllowed).toBe(true); + + // ensure booking button is enabled + expect(ReactDOM.findDOMNode(bookieComponent).querySelector('button').disabled).toBe(false); + + // check markup snapshot + const reactElement = bookieComponent.render(); + const renderedMarkup = ReactDOMServer.renderToStaticMarkup(reactElement); + expect(renderedMarkup).toMatchSnapshot(); + }); + + it('is disabled for the range with a gap', () => { + const state = Object.assign({}, savedState, { + "hourPicked": 16, + "minutesIdxPicked": 2, + "endVal": "2017-01-01T18:15:00+02:00", + }); + + const bookieComponent = createBookieWithState(state); + + expect(bookieComponent.state.bookingAllowed).toBe(false); + expect(ReactDOM.findDOMNode(bookieComponent).querySelector('button').disabled).toBe(true); + + const reactElement = bookieComponent.render(); + const renderedMarkup = ReactDOMServer.renderToStaticMarkup(reactElement); + expect(renderedMarkup).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/components/__snapshots__/Bookie.test.jsx.snap b/src/components/__snapshots__/Bookie.test.jsx.snap new file mode 100644 index 0000000..9dd94c0 --- /dev/null +++ b/src/components/__snapshots__/Bookie.test.jsx.snap @@ -0,0 +1,3 @@ +exports[`enabling bookie button is disabled for the range with a gap 1`] = `""`; + +exports[`enabling bookie button is enabled for valid range 1`] = `""`; diff --git a/src/lib/BusyAdapter.js b/src/lib/BusyAdapter.js index 4bb690d..29d73ae 100644 --- a/src/lib/BusyAdapter.js +++ b/src/lib/BusyAdapter.js @@ -2,15 +2,10 @@ import gonebusy, { CreateBookingBody } from 'gonebusy-nodejs-client/lib'; import { Promise } from 'bluebird'; import Scheduler from './Scheduler'; -import gonebusyEnv from '../../config/gonebusy_env'; - const ServicesController = Promise.promisifyAll(gonebusy.ServicesController); const BookingsController = Promise.promisifyAll(gonebusy.BookingsController); -const { - clientToken: authorization, - clientApiEndpoint -} = gonebusyEnv; +const { REACT_APP_TOKEN: authorization, REACT_APP_API_ENDPOINT: clientApiEndpoint } = process.env; gonebusy.configuration.BASEURI = clientApiEndpoint; diff --git a/src/lib/Scheduler.js b/src/lib/Scheduler.js index a3de043..eec816d 100644 --- a/src/lib/Scheduler.js +++ b/src/lib/Scheduler.js @@ -21,6 +21,10 @@ moment.updateLocale(moment.locale(), { }); class Scheduler { + static getCurrentMoment() { + return moment(); + } + static getDaysFrame(frameStartDate) { const mbase = moment.utc(frameStartDate); const result = []; @@ -80,7 +84,7 @@ class Scheduler { } static getNowStr() { - return moment().startOf('minute').format(); + return Scheduler.getCurrentMoment().startOf('minute').format(); } static getNextDayString(date) { @@ -102,7 +106,7 @@ class Scheduler { let result = ''; if (dateStr) { const mVal = moment(dateStr); - const mToday = moment(); + const mToday = Scheduler.getCurrentMoment(); if (formatDayToString(mToday) === formatDayToString(mVal)) result = mVal.format('ha:mm'); else if (mToday.year() === mVal.year()) diff --git a/src/lib/StateUpdaterForDatePicker.js b/src/lib/StateUpdaterForDatePicker.js index 0a1cb51..52d10b8 100644 --- a/src/lib/StateUpdaterForDatePicker.js +++ b/src/lib/StateUpdaterForDatePicker.js @@ -257,8 +257,8 @@ class StateUpdaterForDatePicker extends StateUpdaterBase { gapFound = !dayData || !dayData.presentSlots[hour] || !~dayData.presentSlots[hour].indexOf(qMinIdx * 15); - if (gapFound) - console.log('gap found', nextMomentStr); + // if (gapFound) + // console.log('gap found', nextMomentStr); } while (!gapFound && Scheduler.isAfterMin(day, hour, qMinIdx * 15, endVal)); bookingAllowed = !gapFound; } diff --git a/src/lib/__mocks__/BusyAdapter.js b/src/lib/__mocks__/BusyAdapter.js new file mode 100644 index 0000000..c6e3d5a --- /dev/null +++ b/src/lib/__mocks__/BusyAdapter.js @@ -0,0 +1,20 @@ +// const m = jest.genMockFromModule('../BusyAdapter'); + +const mockData = require('./BusyAdapter.test.data'); + +class BusyAdapterMock { + + static getServiceInfoAsync() { + return new Promise((resolve) => { + resolve(mockData.serviceInfo); + }); + } + + static getSlotsAsync(date) { + return new Promise((resolve) => { + resolve(mockData.slotsReturned); + }); + } +} + +export default BusyAdapterMock; diff --git a/src/lib/__mocks__/BusyAdapter.test.data.js b/src/lib/__mocks__/BusyAdapter.test.data.js new file mode 100644 index 0000000..e45c930 --- /dev/null +++ b/src/lib/__mocks__/BusyAdapter.test.data.js @@ -0,0 +1,101 @@ +const serviceInfo = { + // id: 7891245607, + // name: "Dog walking" + id: 111000022, + name: "Service Name for tests 11" +}; + +const slotsReturned = [ + "2017-01-02T09:00:00Z", + "2017-01-02T09:15:00Z", + "2017-01-02T09:30:00Z", + "2017-01-02T09:45:00Z", + "2017-01-02T10:00:00Z", + "2017-01-02T10:15:00Z", + "2017-01-02T10:30:00Z", + "2017-01-02T10:45:00Z", + "2017-01-02T11:00:00Z", + "2017-01-02T11:15:00Z", + "2017-01-02T11:30:00Z", + "2017-01-02T11:45:00Z", + "2017-01-02T12:00:00Z", + "2017-01-02T12:15:00Z", + "2017-01-02T12:30:00Z", + "2017-01-02T12:45:00Z", + "2017-01-02T13:00:00Z", + "2017-01-02T13:15:00Z", + "2017-01-02T13:30:00Z", + "2017-01-02T13:45:00Z", + "2017-01-02T14:00:00Z", + "2017-01-02T14:15:00Z", + "2017-01-02T14:30:00Z", + "2017-01-02T14:45:00Z", + "2017-01-02T15:00:00Z", + "2017-01-02T15:15:00Z", + "2017-01-02T15:30:00Z", + "2017-01-02T15:45:00Z", + "2017-01-02T16:00:00Z", + "2017-01-02T16:15:00Z", + "2017-01-02T16:30:00Z", + "2017-01-02T16:45:00Z", + "2017-01-02T17:00:00Z", + "2017-01-02T17:15:00Z", + "2017-01-02T17:30:00Z", + "2017-01-02T17:45:00Z", + "2017-01-02T18:00:00Z", + "2017-01-02T18:15:00Z", + "2017-01-02T18:30:00Z", + "2017-01-02T18:45:00Z", + "2017-01-02T19:00:00Z", + "2017-01-02T19:15:00Z", + "2017-01-02T19:30:00Z", + "2017-01-02T19:45:00Z", + "2017-01-03T09:00:00Z", + "2017-01-03T09:15:00Z", + "2017-01-03T09:30:00Z", + "2017-01-03T09:45:00Z", + "2017-01-03T10:00:00Z", + "2017-01-03T10:15:00Z", + "2017-01-03T10:30:00Z", + "2017-01-03T10:45:00Z", + "2017-01-03T11:00:00Z", + "2017-01-03T11:15:00Z", + "2017-01-03T11:30:00Z", + "2017-01-03T11:45:00Z", + "2017-01-03T12:00:00Z", + "2017-01-03T12:15:00Z", + "2017-01-03T12:30:00Z", + "2017-01-03T12:45:00Z", + "2017-01-03T13:00:00Z", + "2017-01-03T13:15:00Z", + "2017-01-03T13:30:00Z", + "2017-01-03T13:45:00Z", + "2017-01-03T14:00:00Z", + "2017-01-03T14:15:00Z", + "2017-01-03T14:30:00Z", + "2017-01-03T14:45:00Z", + "2017-01-03T15:00:00Z", + "2017-01-03T15:15:00Z", + "2017-01-03T15:30:00Z", + "2017-01-03T15:45:00Z", + "2017-01-03T16:00:00Z", + "2017-01-03T16:15:00Z", + "2017-01-03T16:30:00Z", + "2017-01-03T16:45:00Z", + "2017-01-03T17:00:00Z", + "2017-01-03T17:15:00Z", + "2017-01-03T17:30:00Z", + "2017-01-03T17:45:00Z", + "2017-01-03T18:00:00Z", + "2017-01-03T18:15:00Z", + "2017-01-03T18:30:00Z", + "2017-01-03T18:45:00Z", + "2017-01-03T19:00:00Z", + "2017-01-03T19:15:00Z", + "2017-01-03T19:30:00Z", + "2017-01-03T19:45:00Z"]; + +module.exports = { + serviceInfo, + slotsReturned +}; diff --git a/src/lib/__mocks__/Scheduler.js b/src/lib/__mocks__/Scheduler.js new file mode 100644 index 0000000..83d3006 --- /dev/null +++ b/src/lib/__mocks__/Scheduler.js @@ -0,0 +1,6 @@ +const moment = require('moment'); +const schedulerInstance = require.requireActual('../Scheduler').default; + +schedulerInstance.getCurrentMoment = (() => moment('2017-01-02 17:00')); + +export default schedulerInstance;