UNPKG

@nguyenmv2/buy-button

Version:

BuyButton.js allows merchants to build Shopify interfaces into any website

460 lines (407 loc) 15.8 kB
import Component from '../../src/component'; import View from '../../src/view'; import Updater from '../../src/updater'; import * as componentDefaults from '../../src/defaults/components'; import * as logNotFound from '../../src/utils/log-not-found'; import * as logger from '../../src/utils/logger'; import * as isFunction from '../../src/utils/is-function'; import defaultMoneyFormat from '../../src/defaults/money-format'; describe('Component class', () => { describe('constructor', () => { let component; let componentDefaultsStub; const config = { id: 'id', handle: 'handle', storefrontId: 'sfid', debug: 'debug', cartNode: 'cartNode', modalNode: 'modalNode', toggles: 'toggles', node: document.createElement('div'), options: { product: { buttonDestination: 'modal', }, }, }; const props = 'props'; const componentDefault = 'default'; beforeEach(() => { componentDefaultsStub = sinon.stub(componentDefaults, 'default').value({componentDefault}); component = new Component(config, props); }); afterEach(() => { componentDefaultsStub.restore(); }); it('sets id, storeFrontId, node, and handle on instance', () => { assert.equal(component.id, config.id); assert.equal(component.handle, config.handle); assert.equal(component.storefrontId, config.storefrontId); assert.equal(component.node, config.node); }); it('sets globalConfig based on passed in config', () => { const expectedObj = { debug: config.debug, cartNode: config.cartNode, moneyFormat: decodeURIComponent(defaultMoneyFormat), modalNode: config.modalNode, toggles: config.toggles, }; assert.deepEqual(component.globalConfig, expectedObj); }); it('sets moneyFormat to decoded moneyFormat from config if it exists', () => { config.moneyFormat = encodeURIComponent('$${{amount}}'); component = new Component(config); assert.equal(component.globalConfig.moneyFormat, decodeURIComponent('$${{amount}}')); }); it('instantiates a view', () => { assert.instanceOf(component.view, View); }); it('instantiates an updater', () => { assert.instanceOf(component.updater, Updater); }); it('sets config from merging config.options with componentDefaults', () => { assert.equal(component.config.product.buttonDestination, config.options.product.buttonDestination); assert.equal(component.config.componentDefault, componentDefault); }); it('sets props from props passed in', () => { assert.equal(component.props, props); }); it('instantiates an empty model object', () => { assert.deepEqual(component.model, {}); }); }); describe('prototype methods', () => { let component; const fetchData = {test: 'fetchData'}; beforeEach(() => { Component.prototype.typeKey = 'product'; Component.prototype.fetchData = sinon.stub().resolves(fetchData); component = new Component({id: 1234}); }); describe('init()', () => { let viewInitStub; let renderSpy; let delegateEventsSpy; const data = {data: 'data'}; beforeEach(() => { viewInitStub = sinon.stub().resolves(); renderSpy = sinon.spy(); delegateEventsSpy = sinon.spy(); component.view = { init: viewInitStub, render: renderSpy, delegateEvents: delegateEventsSpy, }; }); describe('successful initialization', () => { let userEventStub; let setupModelStub; let setupModel; beforeEach(() => { setupModel = {data: 'setupModel'}; userEventStub = sinon.stub(component, '_userEvent'); setupModelStub = sinon.stub(component, 'setupModel').resolves(setupModel); }); afterEach(() => { userEventStub.restore(); setupModelStub.restore(); }); it('assigns model and initializes view', async () => { await component.init(data); assert.equal(component.model, setupModel); assert.calledOnce(renderSpy); assert.calledOnce(setupModelStub); assert.calledWith(setupModelStub, data); assert.calledOnce(delegateEventsSpy); assert.calledOnce(viewInitStub); }); it('calls userEvent for beforeInit and afterInit', async () => { await component.init(data); assert.calledTwice(userEventStub); assert.calledWith(userEventStub.getCall(0), 'beforeInit'); assert.calledOnce(viewInitStub); assert.calledWith(userEventStub.getCall(1), 'afterInit'); }); it('returns the component instance', async () => { const response = await component.init(data); assert.equal(response, component); }); }); describe('unsuccessful initialization', () => { let errorSetupModelStub; let logNotFoundStub; beforeEach(() => { logNotFoundStub = sinon.stub(logNotFound, 'default'); }); afterEach(() => { errorSetupModelStub.restore(); logNotFoundStub.restore(); }); it('catches and throws any error from setupModel', async () => { const setupError = {message: ['test']}; errorSetupModelStub = sinon.stub(component, 'setupModel').rejects(setupError); try { await component.init(data); } catch (error) { assert.equal(error, setupError); } assert.throws(component.init, Error); }); it('logs a not found error if the error message contains "Not Found"', async () => { const setupError = {message: ['Not Found']}; errorSetupModelStub = sinon.stub(component, 'setupModel').rejects(setupError); try { await component.init(data); } catch (error) { assert.equal(error, setupError); } assert.calledOnce(logNotFoundStub); assert.calledWith(logNotFoundStub, component); }); it('does not log a not found error if the error message does not contain "Not Found"', async () => { const setupError = {message: ['Another Error']}; errorSetupModelStub = sinon.stub(component, 'setupModel').rejects(setupError); try { await component.init(data); } catch (error) { assert.equal(error, setupError); } assert.notCalled(logNotFoundStub); }); }); }); describe('setupModel()', () => { it('returns passed data', async () => { const data = {test: 'test'}; const model = await component.setupModel(data); assert.deepEqual(model, data); }); it('fetches data if data was not passed', async () => { const model = await component.setupModel(); assert.calledOnce(component.fetchData); assert.deepEqual(model, fetchData); }); }); describe('updateConfig()', () => { it('updates config with config param', () => { const config = 'config'; const updatedConfig = 'updated'; const updateConfigStub = sinon.stub(component.updater, 'updateConfig').returns(updatedConfig); const returnVal = component.updateConfig(config); assert.calledWith(updateConfigStub, config); assert.equal(returnVal, updatedConfig); updateConfigStub.restore(); }); }); describe('destroy()', () => { it('destroys the view', () => { const destroyStub = sinon.stub(component.view, 'destroy'); component.destroy(); assert.calledOnce(destroyStub); destroyStub.restore(); }); }); describe('getters', () => { describe('name', () => { it('returns name based on id if it exists', () => { component.id = 'id'; assert.equal(component.name, 'frame-product-id'); }); it('returns name based on handle if id does not exist', () => { component.handle = 'handle'; component.id = null; assert.equal(component.name, 'frame-product-handle'); }); it('returns name based on typeKey', () => { component.typeKey = 'typeKey'; assert.equal(component.name, 'frame-typeKey-1234'); }); }); describe('options', () => { it('returns options for component by typeKey', () => { assert.deepEqual(component.options, component.config.product); }); }); describe('options dependent', () => { beforeEach(() => { component = Object.defineProperty(component, 'options', { value: { DOMEvents: 'DOMEvents', events: 'events', viewData: {viewData: 'viewData'}, text: 'text', manifest: ['manifest1', 'manifest2'], }, }); component.config = { manifest1: { classes: { label: 'manifest1-label', name: 'manifest1-name', }, styles: { button: {color: 'red'}, }, googleFonts: ['Arial'], }, manifest2: { classes: { label: 'manifest2-label', name: 'manifest2-name', }, styles: { div: {color: 'blue'}, }, googleFonts: ['Calibri'], }, }; }); describe('DOMEvents', () => { it('returns options.DOMEvents if it exists', () => { assert.equal(component.DOMEvents, 'DOMEvents'); }); it('returns an empty object if options.DOMEvents does not exist', () => { component.options.DOMEvents = null; assert.deepEqual(component.DOMEvents, {}); }); }); describe('events', () => { it('returns options.events if it exists', () => { assert.equal(component.events, 'events'); }); it('returns an empty object if options.events does not exist', () => { component.options.events = null; assert.deepEqual(component.events, {}); }); }); describe('styles', () => { it('returns styles for each component in manifest', () => { const expectedObj = { manifest1: { button: component.config.manifest1.styles.button, }, manifest2: { div: component.config.manifest2.styles.div, }, }; assert.deepEqual(component.styles, expectedObj); }); }); describe('classes', () => { it('returns classes for each component in manifest', () => { const expectedObj = { manifest1: { label: component.config.manifest1.classes.label, name: component.config.manifest1.classes.name, }, manifest2: { label: component.config.manifest2.classes.label, name: component.config.manifest2.classes.name, }, }; assert.deepEqual(component.classes, expectedObj); }); }); describe('selectors', () => { it('returns classes formatted as css selectors for each component in manifest', () => { const expectedObj = { manifest1: { label: `.${component.config.manifest1.classes.label}`, name: `.${component.config.manifest1.classes.name}`, }, manifest2: { label: `.${component.config.manifest2.classes.label}`, name: `.${component.config.manifest2.classes.name}`, }, }; assert.deepEqual(component.selectors, expectedObj); }); }); describe('googleFonts', () => { it('returns google fonts for each component in manifest', () => { const googleFonts1 = component.config.manifest1.googleFonts; const googleFonts2 = component.config.manifest2.googleFonts; assert.deepEqual(component.googleFonts, [...googleFonts1, ...googleFonts2]); }); }); describe('viewData', () => { it('returns merged object of model, viewData, classes, and text', () => { component.model = {model: 'model'}; component = Object.defineProperty(component, 'classes', { value: 'classes', }); const expectedObj = { viewData: component.options.viewData.viewData, text: component.options.text, model: component.model.model, classes: component.classes, }; assert.deepEqual(component.viewData, expectedObj); }); }); }); describe('morphCallbacks', () => { it('returns an object with the function onBeforeElUpdated', () => { assert.instanceOf(component.morphCallbacks, Object); assert.equal(Object.keys(component.morphCallbacks).length, 1); assert.instanceOf(component.morphCallbacks.onBeforeElUpdated, Function); }); describe('onBeforeElUpdated()', () => { it('returns false if fromEl\'s tagname is img and its source is toEl\'s data-src element', () => { const fromEl = {tagName: 'IMG', src: 'data-src'}; const toEl = { getAttribute(param) { return param; }, }; assert.equal(component.morphCallbacks.onBeforeElUpdated(fromEl, toEl), false); }); it('returns true if fromEl\'s tagname is not img or its source is not toEl\'s data-src element', () => { const fromEl = {tagName: 'not IMG', src: 'not data-src'}; const toEl = { getAttribute(param) { return param; }, }; assert.equal(component.morphCallbacks.onBeforeElUpdated(fromEl, toEl), true); }); }); }); }); describe('"private" methods', () => { describe('_userEvent()', () => { it('logs to logger if debug is set to true', () => { const infoSpy = sinon.spy(); const loggerStub = sinon.stub(logger, 'default').value({info: infoSpy}); component.globalConfig.debug = true; component.typeKey = 'key'; component._userEvent('test'); assert.calledOnce(infoSpy); assert.calledWith(infoSpy, 'EVENT: test (key)'); loggerStub.restore(); }); it('does not log if debug is set to false', () => { const infoSpy = sinon.spy(); const loggerStub = sinon.stub(logger, 'default').value({info: infoSpy}); component.globalConfig.debug = false; component._userEvent('test'); assert.notCalled(infoSpy); loggerStub.restore(); }); it('calls event if the method passed is a function in the event', () => { const eventSpy = sinon.spy(); component = Object.defineProperty(component, 'events', { value: {test: eventSpy}, }); const isFunctionStub = sinon.stub(isFunction, 'default').returns(true); component._userEvent('test'); assert.calledOnce(eventSpy); assert.calledWith(eventSpy, component); isFunctionStub.restore(); }); }); }); }); });