UNPKG

@nguyenmv2/buy-button

Version:

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

838 lines (721 loc) 27.6 kB
import View from '../../src/view'; import Component from '../../src/component'; import Template from '../../src/template'; import Iframe from '../../src/iframe'; import * as elementClass from '../../src/utils/element-class'; import * as focusUtils from '../../src/utils/focus'; describe('View class', () => { describe('constructor', () => { let component; let view; beforeEach(() => { component = new Component({ id: 1234, node: document.createElement('div'), }); view = new View(component); }); it('stores component to instance', () => { assert.equal(view.component, component); }); it('sets iframe to null', () => { assert.equal(view.iframe, null); }); it('sets node to component\'s node', () => { assert.equal(view.node, component.node); }); it('creates a template instance', () => { assert.instanceOf(view.template, Template); }); it('sets eventBound to false', () => { assert.equal(view.eventsBound, false); }); }); describe('prototype methods', () => { let component; let view; beforeEach(() => { component = new Component({ id: 1234, node: document.createElement('div'), }, {browserFeatures: {}}); view = new View(component); component.typeKey = 'product'; }); describe('init()', () => { let loadStub; let addClassStub; const loadRes = 'done'; beforeEach(() => { loadStub = sinon.stub(Iframe.prototype, 'load').resolves(loadRes); addClassStub = sinon.stub(Iframe.prototype, 'addClass'); component = Object.defineProperty(component, 'options', { writable: true, value: { iframe: true, manifest: ['product', 'option'], }, }); }); afterEach(() => { loadStub.restore(); addClassStub.restore(); }); it('returns a promise if iframe option is false', async () => { component.options.iframe = false; const iframe = await view.init(); assert.isNull(iframe); }); it('returns the iframe if it already exists', async () => { view.iframe = 'iframe'; const iframe = await view.init(); assert.equal(iframe, view.iframe); }); it('creates and loads an Iframe', async () => { await view.init(); assert.instanceOf(view.iframe, Iframe); assert.calledOnce(loadStub); }); it('returns the response of iframe\'s load()', async () => { const response = await view.init(); assert.equal(response, loadRes); }); it('adds view\'s className to iframe', async () => { await view.init(); assert.calledOnce(addClassStub); assert.calledWith(addClassStub, view.className); }); it('adds class name dependent on typeKey to component node', async () => { component.typeKey = 'typeKey'; await view.init(); assert.equal(component.node.className, ' shopify-buy-frame shopify-buy-frame--typeKey'); }); }); describe('render()', () => { let userEventStub; let templateRenderStub; let wrapTemplateStub; let createWrapperStub; let updateNodeStub; let resizeStub; let div; let htmlTemplate; let wrapTemplateReturnVal; beforeEach(() => { div = document.createElement('div'); htmlTemplate = '<div>test</div>'; wrapTemplateReturnVal = 'wrapped'; userEventStub = sinon.stub(component, '_userEvent'); templateRenderStub = sinon.stub(view.template, 'render').returns(htmlTemplate); wrapTemplateStub = sinon.stub(view, 'wrapTemplate').returns(wrapTemplateReturnVal); createWrapperStub = sinon.stub(view, '_createWrapper').returns(div); updateNodeStub = sinon.stub(view, 'updateNode'); resizeStub = sinon.stub(view, 'resize'); }); afterEach(() => { userEventStub.restore(); templateRenderStub.restore(); wrapTemplateStub.restore(); createWrapperStub.restore(); updateNodeStub.restore(); resizeStub.restore(); }); it('renders template by passing in viewData and callback function to template\'s render', () => { view.render(); assert.calledOnce(templateRenderStub); assert.deepEqual(templateRenderStub.getCall(0).args[0], {data: component.viewData}); const renderCb = templateRenderStub.getCall(0).args[1]('test'); assert.calledOnce(wrapTemplateStub); assert.calledWith(wrapTemplateStub, 'test'); assert.equal(renderCb, wrapTemplateReturnVal); }); it('calls component\'s user event before and after render', () => { view.render(); assert.calledTwice(userEventStub); assert.calledWith(userEventStub.getCall(0), 'beforeRender'); assert.calledWith(userEventStub.getCall(1), 'afterRender'); assert.calledOnce(templateRenderStub); }); it('creates a wrapper if one does not already exist', () => { view.wrapper = null; view.render(); assert.calledOnce(createWrapperStub); assert.equal(view.wrapper, div); }); it('does not create a new wrapper if one already exists', () => { view.wrapper = document.createElement('div'); view.render(); assert.notCalled(createWrapperStub); }); it('updates node with wrapper and html then resizes', () => { view.render(); assert.calledOnce(updateNodeStub); assert.calledWith(updateNodeStub, view.wrapper, htmlTemplate); assert.calledOnce(resizeStub); }); }); describe('delegateEvents()', () => { let addEventListenerSpy; let clickBtnSpy; let clickSpy; let closeComponentsOnEscStub; let onStub; beforeEach(() => { addEventListenerSpy = sinon.spy(); clickBtnSpy = sinon.spy(); clickSpy = sinon.spy(); component = Object.defineProperty(component, 'DOMEvents', { value: { 'click .btn': clickBtnSpy, click: clickSpy, }, }); view.wrapper = { addEventListener: addEventListenerSpy, }; closeComponentsOnEscStub = sinon.stub(view, 'closeComponentsOnEsc'); onStub = sinon.stub(view, '_on'); }); afterEach(() => { closeComponentsOnEscStub.restore(); onStub.restore(); }); it('calls closeComponentsOnEsc()', () => { view.delegateEvents(); assert.calledOnce(closeComponentsOnEscStub); }); it('calls _on for each DOM event with a selector', () => { const event = new Event('click'); const target = document.createElement('div'); view.delegateEvents(); assert.calledOnce(onStub); assert.calledWith(onStub, 'click', '.btn'); onStub.getCall(0).args[2](event, target); assert.calledOnce(clickBtnSpy); assert.calledWith(clickBtnSpy, event, target); }); it('adds event listener for DOM event without a selector', () => { const event = new Event('click'); view.delegateEvents(); assert.calledWith(addEventListenerSpy, 'click'); addEventListenerSpy.getCall(0).args[1](event); assert.calledOnce(clickSpy); assert.calledWith(clickSpy, event); }); it('binds events and sets eventsBound to true if eventsBound is false', () => { view.eventsBound = false; view.delegateEvents(); assert.called(closeComponentsOnEscStub); assert.called(onStub); assert.called(addEventListenerSpy); assert.equal(view.eventsBound, true); }); it('prevents rebinding if events are already bound', () => { view.eventsBound = true; assert.notCalled(closeComponentsOnEscStub); assert.notCalled(onStub); assert.notCalled(addEventListenerSpy); }); it('sets iframe.el.onload to null and reloads iframe when iframe.el.onload is called if iframe already exists', () => { const reloadIframeStub = sinon.stub(view, 'reloadIframe'); view.iframe = {el: {}}; view.delegateEvents(); assert.instanceOf(view.iframe.el.onload, Function); view.iframe.el.onload(); assert.equal(view.iframe.el.onload, null); assert.calledOnce(reloadIframeStub); reloadIframeStub.restore(); }); }); describe('reloadIframe()', () => { it('removes iframe and initializes component', () => { const el = document.createElement('div'); view.iframe = {el}; view.wrapper = {}; const removeChildStub = sinon.stub(view.node, 'removeChild'); const initStub = sinon.stub(view.component, 'init'); view.reloadIframe(); assert.calledOnce(removeChildStub); assert.calledWith(removeChildStub, el); assert.isNull(view.wrapper); assert.isNull(view.iframe); assert.calledOnce(initStub); removeChildStub.restore(); initStub.restore(); }); }); describe('append()', () => { it('appends to document if iframe exists', () => { const appendChildSpy = sinon.spy(); const div = document.createElement('div'); view.iframe = { document: { body: { appendChild: appendChildSpy, }, }, }; view.append(div); assert.calledOnce(appendChildSpy); assert.calledWith(appendChildSpy, div); }); it('appends to node if iframe does not exist', () => { const div = document.createElement('div'); const appendChildStub = sinon.stub(view.component.node, 'appendChild'); view.iframe = null; view.append(div); assert.calledOnce(appendChildStub); assert.calledWith(appendChildStub, div); appendChildStub.restore(); }); }); describe('addClass()', () => { let addClassToElementStub; let addClassSpy; beforeEach(() => { addClassSpy = sinon.spy(); view.iframe = {addClass: addClassSpy}; addClassToElementStub = sinon.stub(elementClass, 'addClassToElement'); }); afterEach(() => { addClassToElementStub.restore(); }); it('adds class to iframe if iframe exists', () => { view.addClass('test-class'); assert.calledOnce(addClassSpy); assert.calledWith(addClassSpy, 'test-class'); assert.notCalled(addClassToElementStub); }); it('adds class to element if iframe does not exist', () => { view.iframe = null; view.addClass('test-class'); assert.calledOnce(addClassToElementStub); assert.calledWith(addClassToElementStub, 'test-class', view.component.node); }); }); describe('removeClass()', () => { let removeClassFromElementStub; let removeClassSpy; beforeEach(() => { removeClassSpy = sinon.spy(); view.iframe = {removeClass: removeClassSpy}; removeClassFromElementStub = sinon.stub(elementClass, 'removeClassFromElement'); }); afterEach(() => { removeClassFromElementStub.restore(); }); it('removes class from iframe if iframe exists', () => { view.removeClass('test-class'); assert.calledOnce(removeClassSpy); assert.calledWith(removeClassSpy, 'test-class'); assert.notCalled(removeClassFromElementStub); }); it('removes class from element if iframe does not exist', () => { view.iframe = null; view.removeClass('test-class'); assert.calledOnce(removeClassFromElementStub); assert.calledWith(removeClassFromElementStub, 'test-class', view.component.node); }); }); describe('destroy()', () => { it('removes node from parent', () => { const removeChildSpy = sinon.spy(); view.node = { parentNode: { removeChild: removeChildSpy, }, }; view.destroy(); assert.calledOnce(removeChildSpy); assert.calledWith(removeChildSpy, view.node); }); }); describe('renderChild()', () => { it('updates node with new node and html created from class name and template params', () => { const html = '<h1>test</h1>'; const node = document.createElement('div'); const renderStub = sinon.stub().returns(html); const querySelectorStub = sinon.stub().returns(node); const template = { render: renderStub, }; view.wrapper = { querySelector: querySelectorStub, }; const updateNodeStub = sinon.stub(view, 'updateNode'); view.renderChild('class1 class2', template); assert.calledOnce(querySelectorStub); assert.calledWith(querySelectorStub, '.class1.class2'); assert.calledOnce(renderStub); assert.deepEqual(renderStub.getCall(0).args[0], {data: view.component.viewData}); assert.calledOnce(updateNodeStub); assert.calledWith(updateNodeStub, node, html); updateNodeStub.restore(); }); }); describe('updateNode()', () => { it('updates contents of node', () => { const div = document.createElement('div'); div.innerHTML = '<h1>old</h1>'; const html = '<h1>new</h1>'; view.updateNode(div, `<div>${html}</div>`); assert.equal(div.innerHTML, html); }); }); describe('wrapTemplate()', () => { it('puts strings in a div with typeKey\'s class and html', () => { component.typeKey = 'typeKey'; component = Object.defineProperty(component, 'classes', { value: {typeKey: {typeKey: 'testClass'}}, }); const string = view.wrapTemplate('test'); assert.equal(string, '<div class="testClass">test</div>'); }); }); describe('resize()', () => { let resizeXStub; let resizeYStub; beforeEach(() => { resizeXStub = sinon.stub(view, '_resizeX'); resizeYStub = sinon.stub(view, '_resizeY'); }); afterEach(() => { resizeXStub.restore(); resizeYStub.restore(); }); it('does not resize if there is no iframe', () => { view.iframe = null; view.wrapper = {}; view.resize(); assert.notCalled(resizeXStub); assert.notCalled(resizeYStub); }); it('does not resize if there is no wrapper', () => { view.iframe = {}; view.wrapper = null; view.resize(); assert.notCalled(resizeXStub); assert.notCalled(resizeYStub); }); it('resizes iframe width if shouldResizeX is true', () => { view.iframe = {}; view.wrapper = document.createElement('div'); view = Object.defineProperty(view, 'shouldResizeX', { value: true, }); view.resize(); assert.calledOnce(resizeXStub); assert.notCalled(resizeYStub); }); it('resizes iframe height if shouldResizeY is true', () => { view.iframe = {}; view.wrapper = document.createElement('div'); view = Object.defineProperty(view, 'shouldResizeY', { value: true, }); view.resize(); assert.calledOnce(resizeYStub); assert.notCalled(resizeXStub); }); }); describe('setFocus()', () => { it('calls trapFocus with the view wrapper', () => { const trapFocusStub = sinon.stub(focusUtils, 'trapFocus'); view.setFocus(); assert.calledOnce(trapFocusStub); assert.calledWith(trapFocusStub.firstCall, view.wrapper); trapFocusStub.restore(); }); }); describe('closeComponentsOnEsc()', () => { let event; let closeModalSpy; let closeCartSpy; beforeEach(() => { closeModalSpy = sinon.spy(); closeCartSpy = sinon.spy(); component.props = { closeModal: closeModalSpy, closeCart: closeCartSpy, }; view = Object.defineProperty(view, 'document', { value: document, }); event = new Event('keydown'); }); it('does not add event listener if there is no iframe', () => { view.iframe = null; const addEventListenerStub = sinon.stub(view.document, 'addEventListener'); view.closeComponentsOnEsc(); assert.notCalled(addEventListenerStub); addEventListenerStub.restore(); }); it('closes modal and cart when escape key is pressed', () => { view.iframe = {}; event.keyCode = 27; // escape key view.closeComponentsOnEsc(); view.document.dispatchEvent(event); assert.calledOnce(closeModalSpy); assert.calledOnce(closeCartSpy); }); it('does not close modal or cart when any key except escape is pressed', () => { view.iframe = {}; event.keyCode = 999; view.closeComponentsOnEsc(); view.document.dispatchEvent(event); assert.notCalled(closeModalSpy); assert.notCalled(closeCartSpy); }); }); describe('animateRemoveNode()', () => { let node; let removeNodeStub; let event; let addClassToElementStub; beforeEach(() => { node = document.createElement('div'); node.setAttribute('id', 123); document.body.appendChild(node); removeNodeStub = sinon.stub(view, 'removeNode'); addClassToElementStub = sinon.stub(elementClass, 'addClassToElement'); event = new Event('animationend'); }); afterEach(() => { document.body.removeChild(node); removeNodeStub.restore(); addClassToElementStub.restore(); }); it('adds is-hidden class to element', () => { view.animateRemoveNode(123); assert.calledOnce(addClassToElementStub); assert.calledWith(addClassToElementStub, 'is-hidden', node); }); it('removes node on animationend event if animation is supported and element has a parent node', () => { view.component.props.browserFeatures.animation = true; view.animateRemoveNode(123); node.dispatchEvent(event); assert.calledOnce(removeNodeStub); assert.calledWith(removeNodeStub, node); }); it('removes the node if animation is not supported', () => { component.props.browserFeatures.animation = false; view.animateRemoveNode(123); assert.calledOnce(removeNodeStub); assert.calledWith(removeNodeStub, node); }); }); describe('removeNode()', () => { it('removes node and calls render', () => { const removeChildSpy = sinon.spy(); const renderStub = sinon.stub(view, 'render'); const el = { parentNode: {removeChild: removeChildSpy}, }; view.removeNode(el); assert.calledOnce(removeChildSpy); assert.calledWith(removeChildSpy, el); assert.calledOnce(renderStub); renderStub.restore(); }); }); describe('getters', () => { describe('outerHeight', () => { let getPropertyValueSpy; beforeEach(() => { view.wrapper = document.createElement('div'); getPropertyValueSpy = sinon.spy(CSSStyleDeclaration.prototype, 'getPropertyValue'); }); afterEach(() => { getPropertyValueSpy.restore(); }); it('returns wrapper height if there is no styling on wrapper', () => { view.wrapper = {clientHeight: 10}; const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(null); assert.equal(view.outerHeight, '10px'); getComputedStyleStub.restore(); }); it('returns the height of the wrapper set in style', () => { const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); view.wrapper.style.height = '50px'; assert.equal(view.outerHeight, '50px'); getComputedStyleStub.restore(); }); it('returns wrapper\'s client height if height is not set in style', () => { view.wrapper = { clientHeight: 20, style: { height: '', getPropertyValue: sinon.spy(), }, }; const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); assert.equal(view.outerHeight, '20px'); getComputedStyleStub.restore(); }); it('calls getPropertyValue() twice if height is not set in style', () => { const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); view.wrapper.style.height = ''; assert.equal(view.outerHeight, `${view.wrapper.clientHeight}px`); assert.calledTwice(getPropertyValueSpy); assert.calledWith(getPropertyValueSpy, 'height'); getComputedStyleStub.restore(); }); it('calls getPropertyValue() twice if height is set to 0px in style', () => { const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); view.wrapper.style.height = '0px'; assert.equal(view.outerHeight, '0px'); assert.calledTwice(getPropertyValueSpy); assert.calledWith(getPropertyValueSpy, 'height'); getComputedStyleStub.restore(); }); it('calls getPropertyValue() twice if height is set to auto in style', () => { const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); view.wrapper.style.height = 'auto'; assert.equal(view.outerHeight, 'auto'); assert.calledTwice(getPropertyValueSpy); assert.calledWith(getPropertyValueSpy, 'height'); getComputedStyleStub.restore(); }); it('calls getPropertyValue() once if height is set to a value in style', () => { const getComputedStyleStub = sinon.stub(window, 'getComputedStyle').returns(view.wrapper.style); view.wrapper.style.height = '30px'; assert.equal(view.outerHeight, '30px'); assert.calledOnce(getPropertyValueSpy); assert.calledWith(getPropertyValueSpy, 'height'); getComputedStyleStub.restore(); }); }); describe('className', () => { it('returns an empty string', () => { assert.equal(view.className, ''); }); }); describe('shouldResizeX', () => { it('returns false', () => { assert.equal(view.shouldResizeX, false); }); }); describe('shouldResizeY', () => { it('returns false', () => { assert.equal(view.shouldResizeY, false); }); }); describe('document', () => { it('returns iframe\'s document if iframe exists', () => { view.iframe = {document: {}}; assert.equal(view.document, view.iframe.document); }); it('returns window\'s document if iframe does not exist', () => { view.iframe = null; assert.equal(view.document, window.document); }); }); }); describe('"private" methods', () => { describe('_createWrapper()', () => { let createElementStub; let appendStub; let mockWrapper; beforeEach(() => { mockWrapper = document.createElement('div'); createElementStub = sinon.stub(document, 'createElement').returns(mockWrapper); appendStub = sinon.stub(view, 'append'); }); afterEach(() => { createElementStub.restore(); appendStub.restore(); }); it('creates a wrapper and appends it to view', () => { view._createWrapper(); assert.calledOnce(createElementStub); assert.calledWith(createElementStub, 'div'); assert.calledOnce(appendStub); assert.calledWith(appendStub, mockWrapper); }); it('returns the newly created wrapper', () => { assert.equal(view._createWrapper(), mockWrapper); }); it('adds component type class to wrapper', () => { component.typeKey = 'cart'; component = Object.defineProperty(component, 'classes', { value: { cart: {cart: 'class-name'}, }, }); assert.equal(view._createWrapper().className, 'class-name'); }); }); describe('_resizeX()', () => { it('sets the iframe width to the body\'s client width', () => { view.iframe = {el: {style: {}}}; view = Object.defineProperty(view, 'document', { value: { body: {clientWidth: 10}, }, }); view._resizeX(); assert.equal(view.iframe.el.style.width, '10px'); }); }); describe('_resizeY()', () => { beforeEach(() => { view.iframe = {el: {style: {}}}; }); it('sets the iframe height to param value', () => { view._resizeY('20px'); assert.equal(view.iframe.el.style.height, '20px'); }); it('sets the iframe height to outer height if param is not passed in', () => { view = Object.defineProperty(view, 'outerHeight', { value: '30px', }); view._resizeY(); assert.equal(view.iframe.el.style.height, '30px'); }); }); describe('_on()', () => { describe('addEventListener tests', () => { let addEventListenerSpy; beforeEach(() => { addEventListenerSpy = sinon.spy(); view.wrapper = {addEventListener: addEventListenerSpy}; }); it('calls addEventListener with eventName', () => { view._on('test', '', ''); assert.calledOnce(addEventListenerSpy); assert.calledWith(addEventListenerSpy, 'test'); }); it('executes event handler in capturing phase when event is blur', () => { view._on('blur', '', ''); assert.calledOnce(addEventListenerSpy); assert.calledWith(addEventListenerSpy, 'blur', sinon.match.any, true); }); }); describe('event listener tests', () => { let functionSpy; let event; let button; beforeEach(() => { functionSpy = sinon.spy(); event = new Event('click', {bubbles: true}); button = document.createElement('button'); button.className = 'btn'; view.wrapper = document.createElement('div'); view.wrapper.appendChild(button); }); it('calls function on event if target matches selector', () => { view._on('click', '.btn', functionSpy); view.wrapper.firstChild.dispatchEvent(event); assert.calledOnce(functionSpy); assert.calledWith(functionSpy, event, button); }); it('does not call function if target does not match selector', () => { view._on('click', '.not-btn', functionSpy); view.wrapper.firstChild.dispatchEvent(event); assert.notCalled(functionSpy); }); }); }); }); }); });