From df85fad0308ae7c12d77d33d662ce10f4f460308 Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Thu, 18 Jan 2018 20:36:52 -0500 Subject: [PATCH 1/4] unit testing for Queue --- .babelrc | 9 ++++- .gitignore | 3 +- package.json | 6 +-- src/queue.js | 43 +++++++++++++++++++++ src/queue.test.js | 86 ++++++++++++++++++++++++++++++++++++++++++ src/react-freshchat.js | 80 +++++++++++---------------------------- 6 files changed, 162 insertions(+), 65 deletions(-) create mode 100644 src/queue.js create mode 100644 src/queue.test.js diff --git a/.babelrc b/.babelrc index 967bcca..cc1fbe4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,10 @@ { "presets": ["env", "react-app"], "plugins": ["transform-es2015-modules-umd"], - "ignore": ["**/*.test.js"] -} \ No newline at end of file + "ignore": ["**/*.test.js"], + "env": { + "browser": true, + "es6": true, + "jest/globals": true + } +} diff --git a/.gitignore b/.gitignore index a049075..4fbc68b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules package-lock.json -build \ No newline at end of file +build +coverage \ No newline at end of file diff --git a/package.json b/package.json index be5cc29..d23dde9 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", @@ -20,10 +21,7 @@ "type": "git", "url": "git+https://github.com/smartprocure/react-freshchat.git" }, - "keywords": [ - "react", - "freshchat" - ], + "keywords": ["react", "freshchat"], "author": "Giuliano Kranevitter ", "license": "ISC", "bugs": { 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 898bfe8..d241951 100644 --- a/src/react-freshchat.js +++ b/src/react-freshchat.js @@ -1,76 +1,37 @@ 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 => { +let mockMethods = (earlyCalls = earlyCalls, methods) => { let obj = {} methods.forEach(method => { - obj = _.set(method, queueMethod(method), obj) + obj = _.set(method, queueMethod(earlyCalls, method), obj) }) return obj } -let queueMethod = method => (...args) => earlyCalls.queue({ method, args }) +let queueMethod = (earlyCalls, method) => (...args) => + earlyCalls.queue({ method, args }) -let loadScript = () => { - let id = 'freshchat-lib' - if (document.getElementById(id)) return +let loadScript = (widget = window.fcWidget) => { + if (widget) return let script = document.createElement('script') script.async = 'true' script.type = 'text/javascript' script.src = 'https://wchat.freshchat.com/js/widget.js' - script.id = id document.head.appendChild(script) } @@ -78,6 +39,8 @@ class FreshChat extends React.Component { constructor(props) { super(props) + this.win = window || {} + let { token, ...moreProps } = props if (!token) { @@ -92,29 +55,30 @@ class FreshChat extends React.Component { } init(settings) { + let { fcWidget } = this.win if (settings.onInit) { let tmp = settings.onInit settings.onInit = () => tmp(widget()) } - if (window.fcWidget) { - window.fcWidget.init(settings) + if (fcWidget) { + fcWidget.init(settings) } else { this.lazyInit(settings) } } lazyInit(settings) { - window.fcSettings = settings + this.win.fcSettings = settings loadScript() let interval = setInterval(() => { - if (window.fcWidget) { + if (this.win.fcWidget) { clearInterval(interval) try { earlyCalls.dequeueAll((method, value) => { - window.fcWidget[method](...value) + this.win.fcWidget[method](...value) }) } catch (e) { console.error(e) From 222d991d01a0b486c77b78907e3206a0a24d709e Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Thu, 18 Jan 2018 22:10:27 -0500 Subject: [PATCH 2/4] unit testing for functions --- .gitignore | 3 +- src/react-freshchat.js | 9 ++- src/react-freshchat.test.js | 111 ++++++++++++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4fbc68b..b044b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules package-lock.json build -coverage \ No newline at end of file +coverage +.DS_Store \ No newline at end of file diff --git a/src/react-freshchat.js b/src/react-freshchat.js index d241951..4db2459 100644 --- a/src/react-freshchat.js +++ b/src/react-freshchat.js @@ -15,7 +15,10 @@ export let widget = ( return fake } -let mockMethods = (earlyCalls = earlyCalls, methods) => { +export let mockMethods = ( + earlyCalls = earlyCalls, + methods = availableMethods +) => { let obj = {} methods.forEach(method => { obj = _.set(method, queueMethod(earlyCalls, method), obj) @@ -23,10 +26,10 @@ let mockMethods = (earlyCalls = earlyCalls, methods) => { return obj } -let queueMethod = (earlyCalls, method) => (...args) => +export let queueMethod = (earlyCalls, method) => (...args) => earlyCalls.queue({ method, args }) -let loadScript = (widget = window.fcWidget) => { +export let loadScript = (widget = window.fcWidget, document = document) => { if (widget) return let script = document.createElement('script') script.async = 'true' diff --git a/src/react-freshchat.test.js b/src/react-freshchat.test.js index 7c12347..06f8883 100644 --- a/src/react-freshchat.test.js +++ b/src/react-freshchat.test.js @@ -1,5 +1,108 @@ -describe(`TODO: Test`, () => { - it('should pass', () => { - expect(true).toBe(true) +let FreshChat = require('./react-freshchat') +let Queue = require('./queue').default + +test(`widget`, () => { + let { widget } = FreshChat + 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 +}) + +test(`mockMethods`, () => { + let q = new Queue() + let { mockMethods } = FreshChat + 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, + }, + }) +}) + +test(`queueMethod`, () => { + let { queueMethod } = FreshChat + 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'], + }) +}) + +test(`loadScript`, () => { + let { loadScript } = FreshChat + let widget = {} + let document = { + createElement: jest.fn(() => ({})), + head: { + appendChild: jest.fn(), + }, + } + + loadScript(widget) + expect(document.createElement).not.toHaveBeenCalled() + expect(document.head.appendChild).not.toHaveBeenCalled() + + loadScript(null, document) + expect(document.createElement).toHaveBeenCalledWith('script') + expect(document.head.appendChild).toHaveBeenCalledWith({ + async: 'true', + type: 'text/javascript', + src: 'https://wchat.freshchat.com/js/widget.js', + }) +}) From 8a7270bb6b2df62bbd0e20dc0fd5ea665a05bdc4 Mon Sep 17 00:00:00 2001 From: Decrapifier Date: Sat, 10 Feb 2018 15:22:34 +0000 Subject: [PATCH 3/4] Automagically formatted by Duti! https://github.com/smartprocure/duti --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1393a8e..6583388 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "type": "git", "url": "git+https://github.com/smartprocure/react-freshchat.git" }, - "keywords": ["react", "freshchat"], + "keywords": [ + "react", + "freshchat" + ], "author": "Giuliano Kranevitter ", "license": "ISC", "bugs": { From b97cce0a9f40281b625645f0d05015fdfe4dc603 Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Sun, 11 Feb 2018 19:14:20 -0500 Subject: [PATCH 4/4] completing unit testing --- .babelrc | 9 +-- .eslintrc | 3 +- .eslintrc.js | 7 --- package.json | 14 ++++- src/react-freshchat.js | 38 +++++++----- src/react-freshchat.test.js | 113 ++++++++++++++++++++++++++++++++---- 6 files changed, 138 insertions(+), 46 deletions(-) delete mode 100644 .eslintrc.js diff --git a/.babelrc b/.babelrc index cc1fbe4..4ffef06 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,3 @@ { - "presets": ["env", "react-app"], - "plugins": ["transform-es2015-modules-umd"], - "ignore": ["**/*.test.js"], - "env": { - "browser": true, - "es6": true, - "jest/globals": true - } + "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/package.json b/package.json index 1393a8e..be22cc2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "type": "git", "url": "git+https://github.com/smartprocure/react-freshchat.git" }, - "keywords": ["react", "freshchat"], + "keywords": [ + "react", + "freshchat" + ], "author": "Giuliano Kranevitter ", "license": "ISC", "bugs": { @@ -31,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/react-freshchat.js b/src/react-freshchat.js index a19f69c..07585f4 100644 --- a/src/react-freshchat.js +++ b/src/react-freshchat.js @@ -27,7 +27,7 @@ export let queueMethod = (Q, method) => (...args) => { Q.queue({ method, args }) } -export let loadScript = (widget = window.fcWidget, document = document) => { +export let loadScript = (document = document, widget = window.fcWidget) => { let id = 'freshchat-lib' if (widget || document.getElementById(id)) return let script = document.createElement('script') @@ -42,6 +42,10 @@ 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) { @@ -55,12 +59,16 @@ class FreshChat extends React.Component { }) } + mutateOnInit(settings) { + let tmp = settings.onInit + settings.onInit = () => tmp(widget()) + return settings + } + init(settings) { let { fcWidget } = this.win - if (settings.onInit) { - let tmp = settings.onInit - settings.onInit = () => tmp(widget()) - } + + if (settings.onInit) this.mutateOnInit(settings) if (fcWidget) { fcWidget.init(settings) @@ -75,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) - let interval = setInterval(() => { + this.interval = setInterval(this.checkAndInit(settings), 1000) + } + + checkAndInit(settings) { + return () => { if (this.win.fcWidget) { - clearInterval(interval) + clearInterval(this.interval) try { - earlyCalls.dequeueAll((method, value) => { - this.win.fcWidget[method](...value) + earlyCalls.dequeueAll(({ method, args }) => { + this.win.fcWidget[method](...args) }) } catch (e) { console.error(e) @@ -91,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 70a7edb..cb4ea2c 100644 --- a/src/react-freshchat.test.js +++ b/src/react-freshchat.test.js @@ -1,8 +1,17 @@ -let FreshChat = require('./react-freshchat') -let Queue = require('./queue').default +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' -test(`widget`, () => { - let { widget } = FreshChat +Enzyme.configure({ adapter: new Adapter() }) + +describe(`widget`, () => { let fake = 'FAKE' let real = 'REAL' expect(widget(fake, real, [])).toEqual(real) @@ -14,9 +23,8 @@ test(`widget`, () => { }) }) -test(`mockMethods`, () => { +describe(`mockMethods`, () => { let q = new Queue() - let { mockMethods } = FreshChat let fake = mockMethods(q, ['a']) expect(fake).toEqual({ a: jasmine.any(Function), @@ -69,8 +77,7 @@ test(`mockMethods`, () => { }) }) -test(`queueMethod`, () => { - let { queueMethod } = FreshChat +describe(`queueMethod`, () => { let q = new Queue() let spy = jest.spyOn(q, 'queue') @@ -84,8 +91,7 @@ test(`queueMethod`, () => { }) }) -test(`loadScript`, () => { - let { loadScript } = FreshChat +describe(`loadScript`, () => { let widget = {} let document = { getElementById: jest.fn(() => false), @@ -95,11 +101,11 @@ test(`loadScript`, () => { }, } - loadScript(widget) + loadScript(document, widget) expect(document.createElement).not.toHaveBeenCalled() expect(document.head.appendChild).not.toHaveBeenCalled() - loadScript(null, document) + loadScript(document, null) expect(document.createElement).toHaveBeenCalledWith('script') expect(document.head.appendChild).toHaveBeenCalledWith({ id: 'freshchat-lib', @@ -108,3 +114,86 @@ test(`loadScript`, () => { 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) + }) +})