diff --git a/.babelrc b/.babelrc index 967bcca..4ffef06 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,3 @@ { - "presets": ["env", "react-app"], - "plugins": ["transform-es2015-modules-umd"], - "ignore": ["**/*.test.js"] -} \ No newline at end of file + "presets": ["env", "react"] +} diff --git a/.eslintrc b/.eslintrc index be0460c..f2f0e1e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "parserOptions": { "sourceType": "module" }, + "plugins": ["jest"], "env": { "node": true, "es6": true, @@ -13,4 +14,4 @@ "no-extra-semi": 0, "no-console": 0 } -} \ No newline at end of file +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b8590f9..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: 'smartprocure', - parser: 'babel-eslint', - parserOptions: { - sourceType: 'module' - } -} diff --git a/.gitignore b/.gitignore index a049075..b044b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules package-lock.json -build \ No newline at end of file +build +coverage +.DS_Store \ No newline at end of file diff --git a/package.json b/package.json index eac121f..be22cc2 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "jest", "test:watch": "jest --watch", "test:ci": "npm t -- --coverage --json --outputFile=test-results.json", + "coverage": "npm test -- --coverage --collectCoverageFrom=src/**/*.js", "cicoverage": "npm test", "lint": "eslint --ignore-path .gitignore 'react-freshchat.js'", "lint:ci": "npm run lint -- -o lint-results.json -f json", @@ -33,20 +34,25 @@ "dependencies": { "babel-runtime": "^6.26.0", "lodash": "^4.17.4", - "react": "^16.2.0" + "react": "^16.2.0", + "react-dom": "^16.2.0" }, "devDependencies": { "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", "babel-eslint": "^8.0.1", + "babel-jest": "^22.2.2", "babel-plugin-transform-es2015-modules-umd": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.1", "babel-preset-react-app": "^3.1.1", "danger": "~0.18.0", "duti": "latest", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", "eslint": "^4.15.0", "eslint-config-smartprocure": "^1.0.2", - "eslint-plugin-jest": "^21.2.0", + "eslint-plugin-jest": "^21.8.0", "eslint-plugin-prettier": "^2.3.1", "ghooks": "^2.0.0", "jest": "^22.1.2", diff --git a/src/queue.js b/src/queue.js new file mode 100644 index 0000000..93acd17 --- /dev/null +++ b/src/queue.js @@ -0,0 +1,43 @@ +import _ from 'lodash/fp' + +export default class Queue { + constructor() { + this.data = [] + this.index = 0 + } + + queue(value) { + this.data.push(value) + } + + dequeue() { + if (this.index > -1 && this.index < this.data.length) { + let result = this.data[this.index++] + + if (this.isEmpty) { + this.reset() + } + + return result + } + } + + get isEmpty() { + return this.index >= this.data.length + } + + dequeueAll(cb) { + if (!_.isFunction(cb)) { + throw new Error(`Please provide a callback`) + } + + while (!this.isEmpty) { + cb(this.dequeue()) + } + } + + reset() { + this.data.length = 0 + this.index = 0 + } +} diff --git a/src/queue.test.js b/src/queue.test.js new file mode 100644 index 0000000..e797955 --- /dev/null +++ b/src/queue.test.js @@ -0,0 +1,86 @@ +// import Queue from 'queue' +let Queue = require('./queue').default + +test(`queue method`, () => { + let q = new Queue() + expect(q.data).toEqual([]) + expect(q.index).toBe(0) + + q.queue('testing') + + expect(q.data).toEqual(['testing']) + expect(q.index).toBe(0) + + q.queue('asd') + + expect(q.data).toEqual(['testing', 'asd']) + expect(q.index).toBe(0) +}) + +test(`dequeue method`, () => { + let q = new Queue() + q.queue('testing') + q.queue('asd') + + expect(q.dequeue()).toBe('testing') + expect(q.data).toEqual(['testing', 'asd']) + expect(q.index).toBe(1) + + expect(q.dequeue()).toBe('asd') + expect(q.data).toEqual([]) // it should be empty because we reset the queue + expect(q.index).toBe(0) // it should be 0 because we reset the queue + + expect(q.dequeue()).toBe(undefined) +}) + +test(`isEmpty`, () => { + let q = new Queue() + expect(q.isEmpty).toBe(true) + + q.queue('testing') + expect(q.isEmpty).toBe(false) + + q.queue('asd') + expect(q.isEmpty).toBe(false) + + q.dequeue() + expect(q.isEmpty).toBe(false) + + q.dequeue() + expect(q.isEmpty).toBe(true) +}) + +test(`dequeueAll method`, () => { + let q = new Queue() + let cb = jest.fn() + + q.queue('testing') + q.queue('asd') + + expect(() => { + q.dequeueAll() + }).toThrowError('Please provide a callback') + + q.dequeueAll(cb) + + expect(cb).toHaveBeenCalledTimes(2) + expect(cb.mock.calls[0][0]).toBe('testing') + expect(cb.mock.calls[1][0]).toBe('asd') +}) + +test(`reset method`, () => { + let q = new Queue() + + q.queue('testing') + q.queue('asd') + q.queue('123') + + q.dequeue() + + expect(q.index).toBe(1) + + q.reset() + + expect(q.data).toEqual([]) + expect(q.index).toBe(0) +}) diff --git a/src/react-freshchat.js b/src/react-freshchat.js index 02207ca..07585f4 100644 --- a/src/react-freshchat.js +++ b/src/react-freshchat.js @@ -1,85 +1,51 @@ import _ from 'lodash/fp' import React from 'react' - -class Queue { - constructor() { - this.data = [] - this.index = 0 - } - - queue(value) { - this.data.push(value) - } - - dequeue() { - if (this.index > -1 && this.index < this.data.length) { - let result = this.data[this.index++] - - if (this.isEmpty) { - this.reset() - } - - return result - } - } - - get isEmpty() { - return this.index >= this.data.length - } - - dequeueAll(cb) { - if (!_.isFunction(cb)) { - throw new Error(`Please provide a callback`) - } - - while (!this.isEmpty) { - let { method, args } = this.dequeue() - cb(method, args) - } - } - - reset() { - this.data.length = 0 - this.index = 0 - } -} +import Queue from './queue' let fakeWidget let earlyCalls = new Queue() -export let widget = (fake = fakeWidget) => { - if (window.fcWidget) return window.fcWidget - if (!fake) fake = mockMethods(availableMethods) +export let widget = ( + fake = fakeWidget, + real = window.fcWidget, + methods = availableMethods +) => { + if (real) return real + if (!fake) fake = mockMethods(earlyCalls, methods) return fake } -let mockMethods = methods => { +export let mockMethods = (Q = earlyCalls, methods = availableMethods) => { let obj = {} methods.forEach(method => { - obj = _.set(method, queueMethod(method), obj) + obj = _.set(method, queueMethod(Q, method), obj) }) return obj } -let queueMethod = method => (...args) => { - earlyCalls.queue({ method, args }) +export let queueMethod = (Q, method) => (...args) => { + Q.queue({ method, args }) } -let loadScript = () => { +export let loadScript = (document = document, widget = window.fcWidget) => { let id = 'freshchat-lib' - if (document.getElementById(id) || window.fcWidget) return + if (widget || document.getElementById(id)) return let script = document.createElement('script') + script.id = id script.async = 'true' script.type = 'text/javascript' script.src = 'https://wchat.freshchat.com/js/widget.js' - script.id = id document.head.appendChild(script) } class FreshChat extends React.Component { constructor(props) { super(props) - + + this.win = window || {} + this.doc = document || {} + this.checkAndInit = this.checkAndInit.bind(this) + let { token, ...moreProps } = props if (!token) { @@ -93,14 +59,19 @@ class FreshChat extends React.Component { }) } + mutateOnInit(settings) { + let tmp = settings.onInit + settings.onInit = () => tmp(widget()) + return settings + } + init(settings) { - if (settings.onInit) { - let tmp = settings.onInit - settings.onInit = () => tmp(widget()) - } + let { fcWidget } = this.win - if (window.fcWidget) { - window.fcWidget.init(settings) + if (settings.onInit) this.mutateOnInit(settings) + + if (fcWidget) { + fcWidget.init(settings) if (settings.onInit) { settings.onInit() } @@ -112,14 +83,18 @@ class FreshChat extends React.Component { lazyInit(settings) { widget().init(settings) // Can't use window.fcSettings because sometimes it doesn't work - loadScript() + loadScript(this.doc) + + this.interval = setInterval(this.checkAndInit(settings), 1000) + } - let interval = setInterval(() => { - if (window.fcWidget) { - clearInterval(interval) + checkAndInit(settings) { + return () => { + if (this.win.fcWidget) { + clearInterval(this.interval) try { - earlyCalls.dequeueAll((method, value) => { - window.fcWidget[method](...value) + earlyCalls.dequeueAll(({ method, args }) => { + this.win.fcWidget[method](...args) }) } catch (e) { console.error(e) @@ -128,16 +103,12 @@ class FreshChat extends React.Component { settings.onInit() } } - }, 1000) + } } render() { return false } - - componentWillUnmount() { - widget().close() - } } let availableMethods = [ diff --git a/src/react-freshchat.test.js b/src/react-freshchat.test.js index 7c12347..cb4ea2c 100644 --- a/src/react-freshchat.test.js +++ b/src/react-freshchat.test.js @@ -1,5 +1,199 @@ -describe(`TODO: Test`, () => { - it('should pass', () => { - expect(true).toBe(true) +import React from 'react' +import FreshChat, { + widget, + mockMethods, + queueMethod, + loadScript, +} from './react-freshchat' +import Queue from './queue' +import Enzyme, { mount, shallow } from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' + +Enzyme.configure({ adapter: new Adapter() }) + +describe(`widget`, () => { + let fake = 'FAKE' + let real = 'REAL' + expect(widget(fake, real, [])).toEqual(real) + expect(widget(fake, null, [])).toEqual(fake) + expect(widget(null, null, [])).toEqual({}) + expect(widget(null, null, ['a', 'b'])).toEqual({ + a: jasmine.any(Function), + b: jasmine.any(Function), }) -}) \ No newline at end of file +}) + +describe(`mockMethods`, () => { + let q = new Queue() + let fake = mockMethods(q, ['a']) + expect(fake).toEqual({ + a: jasmine.any(Function), + }) + expect(q.isEmpty).toBe(true) + fake.a('testing') + expect(q.isEmpty).toBe(false) + expect(q.dequeue()).toEqual({ + method: 'a', + args: ['testing'], + }) + + q.reset() + + let withAvailableMethods = mockMethods(q) + let fn = jasmine.any(Function) + expect(withAvailableMethods).toEqual({ + close: fn, + destroy: fn, + hide: fn, + init: fn, + isInitialized: fn, + isLoaded: fn, + isOpen: fn, + off: fn, + on: fn, + open: fn, + setConfig: fn, + setExternalId: fn, + setFaqTags: fn, + setTags: fn, + track: fn, + user: { + show: fn, + track: fn, + user: fn, + clear: fn, + create: fn, + get: fn, + isExists: fn, + setEmail: fn, + setFirstName: fn, + setLastName: fn, + setMeta: fn, + setPhone: fn, + setPhoneCountryCode: fn, + setProperties: fn, + update: fn, + }, + }) +}) + +describe(`queueMethod`, () => { + let q = new Queue() + let spy = jest.spyOn(q, 'queue') + + expect(queueMethod(q, 'a')).toEqual(jasmine.any(Function)) + + queueMethod(q, 'a')('testing') + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith({ + method: 'a', + args: ['testing'], + }) +}) + +describe(`loadScript`, () => { + let widget = {} + let document = { + getElementById: jest.fn(() => false), + createElement: jest.fn(() => ({})), + head: { + appendChild: jest.fn(), + }, + } + + loadScript(document, widget) + expect(document.createElement).not.toHaveBeenCalled() + expect(document.head.appendChild).not.toHaveBeenCalled() + + loadScript(document, null) + expect(document.createElement).toHaveBeenCalledWith('script') + expect(document.head.appendChild).toHaveBeenCalledWith({ + id: 'freshchat-lib', + async: 'true', + type: 'text/javascript', + src: 'https://wchat.freshchat.com/js/widget.js', + }) +}) + +describe(`Component`, () => { + const _settings = { + host: 'https://wchat.freshchat.com', + token: 'asd', + } + + describe(`constructor`, () => { + beforeEach(() => { + window.fcWidget = undefined + }) + + it(`should throw an error if the Prop token is not passed`, () => { + expect(() => shallow()).toThrow('token is required') + }) + + it(`should call the 'init' method`, () => { + // We call 'init' when we construct the class, means that we have to mock the 'init' method to be able to spy it and then we have to restore it + + const originalInit = FreshChat.prototype.init + FreshChat.prototype.init = jest.fn() + + shallow() + + expect(FreshChat.prototype.init).toHaveBeenCalled() + + FreshChat.prototype.init = originalInit + }) + + it(`should call fcWidget.init if fcWidget is loaded`, () => { + window.fcWidget = { init: jest.fn() } + const spyLazyInit = jest.spyOn(FreshChat.prototype, 'lazyInit') + + const settings = { token: _settings.token, test: true } + new FreshChat(settings) + expect(window.fcWidget.init).toHaveBeenCalledWith({ + ..._settings, + ...settings, + }) + expect(spyLazyInit).not.toHaveBeenCalled() + }) + + it(`should mutate prop 'onInit' if it was passed`, () => { + window.fcWidget = { init: jest.fn() } + const spyMutateOnInit = jest.spyOn(FreshChat.prototype, 'mutateOnInit') + const onInit = jest.fn() + const component = new FreshChat({ token: _settings.token, onInit }) + expect(spyMutateOnInit).toHaveBeenCalled() + expect(onInit).toHaveBeenCalled() + expect(component.interval).toBe(undefined) + }) + + it(`should call 'lazyInit' if window.fcWidget is undefined`, () => { + const spyLazyInit = jest.spyOn(FreshChat.prototype, 'lazyInit') + const spyInterval = jest.spyOn(window, 'setInterval') + const spyCheckAndInit = jest.spyOn(FreshChat.prototype, 'checkAndInit') + const component = new FreshChat({ token: _settings.token }) + expect(spyLazyInit).toHaveBeenCalled() + expect(spyInterval).toHaveBeenCalledWith(jasmine.any(Function), 1000) + expect(spyCheckAndInit).toHaveBeenCalledWith(_settings) + expect(component.interval).toEqual(jasmine.any(Number)) + }) + }) + + describe(`checkAndInit`, () => { + const component = new FreshChat({ token: _settings.token }) + const { checkAndInit } = component + expect(checkAndInit).toEqual(jasmine.any(Function)) + expect(checkAndInit(_settings)).toEqual(jasmine.any(Function)) + + const spyClearInterval = jest.spyOn(window, 'clearInterval') + + checkAndInit(_settings)() + expect(spyClearInterval).not.toHaveBeenCalled() + + window.fcWidget = { + init: jest.fn(), + } + checkAndInit(_settings)() + + expect(spyClearInterval).toHaveBeenCalledWith(component.interval) + }) +})