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)
+ })
+})