UNPKG

@nguyenmv2/buy-button

Version:

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

1,376 lines (1,177 loc) 41.9 kB
import Cart from '../../src/components/cart'; import CartToggle from '../../src/components/toggle'; import Component from '../../src/component'; import Checkout from '../../src/components/checkout'; import Template from '../../src/template'; import CartUpdater from '../../src/updaters/cart'; import CartView from '../../src/views/cart'; import ShopifyBuy from '../../src/buybutton'; import * as formatMoney from '../../src/utils/money'; import * as elementClass from '../../src/utils/element-class'; import * as focusUtils from '../../src/utils/focus'; let cart; describe('Cart class', () => { const moneyFormat = '${{amount}}'; let closeCartSpy; let trackSpy; beforeEach(() => { closeCartSpy = sinon.spy(); trackSpy = sinon.spy(); cart = new Cart({ options: { cart: { contents: { title: false, note: true, }, text: { notice: 'test', }, }, }, moneyFormat, }, { client: ShopifyBuy.buildClient({ domain: 'test.myshopify.com', storefrontAccessToken: 123, }), browserFeatures: { transition: true, animation: true, transform: true, }, tracker: { trackMethod: (fn) => { return function() { fn(...arguments); }; }, track: trackSpy, }, closeCart: closeCartSpy, }); }); afterEach(() => { cart.destroy(); }); describe('constructor', () => { it('instantiates child template, checkout, toggles, updater, view', () => { assert.instanceOf(cart.childTemplate, Template); assert.instanceOf(cart.checkout, Checkout); assert.instanceOf(cart.updater, CartUpdater); assert.instanceOf(cart.view, CartView); assert.instanceOf(cart.toggles[0], CartToggle); }); }); describe('createToggles()', () => { it('creates and initializes toggle instances for passed in nodes', () => { const cartToggleInitStub = sinon.stub(CartToggle.prototype, 'init').resolves(); cart.toggles = []; const lineItems = [{id: 321}]; cart.model.lineItems = lineItems; const config = { toggles: [{ node: document.body.appendChild(document.createElement('div')), }], }; return cart.createToggles(config).then(() => { assert.equal(cart.toggles.length, 1); assert.calledOnce(cartToggleInitStub); assert.calledWith(cartToggleInitStub, {lineItems}); }); }); }); describe('get lineItems', () => { it('returns line items from the cart model', () => { const lineItems = [ {id: '123'}, {id: '321'}, ]; cart.model = { lineItems, }; assert.equal(cart.lineItems, lineItems); }); it('returns an empty array if cart model is null', () => { cart.model = null; assert.equal(cart.lineItems.length, 0); }); }); describe('get lineItemsHtml', () => { const variantAmount = '5.00'; let formatMoneySpy; beforeEach(() => { formatMoneySpy = sinon.spy(formatMoney, 'default'); }); afterEach(() => { formatMoneySpy.restore(); }); it('calls render and returns an html string', () => { cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity: 1, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: '5.00', currencyCode: 'CAD', }, }, discountAllocations: [], }]; const renderSpy = sinon.spy(cart.childTemplate, 'render'); assert.include(cart.lineItemsHtml, 'data-line-item-id="123"'); assert.calledOnce(renderSpy); }); describe('price without discounts', () => { beforeEach(() => { cart.childTemplate.contents.price = true; cart.childTemplate.contents.priceWithDiscounts = false; }); it('does not render discounts if discounts allocations are present', () => { const quantity = 2; const discountAmount = '2.00'; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [ { discountApplication: { title: 'BOGO', targetSelection: 'ENTITLED', }, allocatedAmount: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; const discountedPrice = fullPrice - discountAmount; assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.notInclude(cartLineItemsHtml, `$${discountedPrice}.00`); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); }); }); describe('price with discounts', () => { beforeEach(() => { cart.childTemplate.contents.price = false; cart.childTemplate.contents.priceWithDiscounts = true; }); it('renders the full price with no discounts, if no discounts allocations are present', () => { const quantity = 2; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); assert.calledTwice(formatMoneySpy); assert.alwaysCalledWith(formatMoneySpy, fullPrice, moneyFormat); }); it('renders the discount information if a discount allocation exists with a target selection of `ENTITLED`', () => { const discountAmount = '1.00'; const discountTitle = 'BOGO'; const quantity = 2; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [ { discountApplication: { title: discountTitle, targetSelection: 'ENTITLED', }, allocatedAmount: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; const discountedPrice = fullPrice - discountAmount; assert.include(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); assert.include(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, `${discountTitle} (-$${discountAmount})`); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.include(cartLineItemsHtml, `$${discountedPrice}.00`); assert.calledThrice(formatMoneySpy); assert.calledWith(formatMoneySpy.firstCall, fullPrice, moneyFormat); assert.calledWith(formatMoneySpy.secondCall, discountAmount, moneyFormat); assert.calledWith(formatMoneySpy.thirdCall, discountedPrice, moneyFormat); }); it('renders the discount information if the discount allocation exists with a target of `EXPLICIT`', () => { const discountAmount = '1.00'; const discountTitle = 'BOGO'; const quantity = 2; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [ { discountApplication: { title: discountTitle, targetSelection: 'EXPLICIT', }, allocatedAmount: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; const discountedPrice = fullPrice - discountAmount; assert.include(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); assert.include(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, `${discountTitle} (-$${discountAmount})`); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.include(cartLineItemsHtml, `$${discountedPrice}.00`); assert.calledThrice(formatMoneySpy); assert.calledWith(formatMoneySpy.firstCall, fullPrice, moneyFormat); assert.calledWith(formatMoneySpy.secondCall, discountAmount, moneyFormat); assert.calledWith(formatMoneySpy.thirdCall, discountedPrice, moneyFormat); }); it('renders the discount information with the code as discount title if the discount allocation exists with a target of `EXPLICIT` and the discount title does not exist', () => { const discountAmount = '1.00'; const discountCode = 'BOGO'; const quantity = 2; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [ { discountApplication: { code: discountCode, targetSelection: 'EXPLICIT', }, allocatedAmount: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; const discountedPrice = fullPrice - discountAmount; assert.include(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); assert.include(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, `${discountCode} (-$${discountAmount})`); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.include(cartLineItemsHtml, `$${discountedPrice}.00`); assert.calledThrice(formatMoneySpy); assert.calledWith(formatMoneySpy.firstCall, fullPrice, moneyFormat); assert.calledWith(formatMoneySpy.secondCall, discountAmount, moneyFormat); assert.calledWith(formatMoneySpy.thirdCall, discountedPrice, moneyFormat); }); it('does not render the discount information if the discount allocation exists with a target of `ALL`', () => { const quantity = 2; cart.lineItemCache = [{ id: 123, title: 'test', variantTitle: 'test2', quantity, variant: { image: { src: 'cdn.shopify.com/image.jpg', }, priceV2: { amount: variantAmount, currencyCode: 'CAD', }, }, discountAllocations: [ { discountApplication: { title: 'BOGO', targetSelection: 'ALL', }, allocatedAmount: { amount: '1.00', currencyCode: 'CAD', }, }, ], }]; const cartLineItemsHtml = cart.lineItemsHtml; const fullPrice = variantAmount * quantity; assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.fullPrice"'); assert.notInclude(cartLineItemsHtml, 'data-element="lineItem.discount"'); assert.include(cartLineItemsHtml, 'data-element="lineItem.price"'); assert.include(cartLineItemsHtml, `$${fullPrice}.00`); assert.calledTwice(formatMoneySpy); assert.alwaysCalledWith(formatMoneySpy, fullPrice, moneyFormat); }); }); }); describe('imageForLineItem', () => { beforeEach(() => { cart.lineItem = { id: 123, title: 'Line Item', variant_title: 'Line Item title', line_price: 20, quantity: 1, variant: { image: {src: 'cdn.shopify.com/variant_image.jpg'}, }, }; }); it('returns the variant image if it exists', () => { const clientReturn = sinon.stub(cart.props.client.image.helpers, 'imageForSize').returns('cdn.shopify.com/variant_image.jpg'); const variantImage = cart.lineItem.variant.image; const sourceReturned = cart.imageForLineItem(cart.lineItem); assert.calledOnce(clientReturn); assert.calledWith(clientReturn, variantImage); assert.equal(variantImage.src, sourceReturned); }); afterEach(() => { cart.props.client.image.helpers.imageForSize.restore(); }); }); describe('fetchData()', () => { it('resolves to null if key does not exist in localStorage', () => { localStorage.removeItem(cart.localStorageCheckoutKey); return cart.fetchData().then((data) => { assert.equal(data, null); }); }); it('calls fetch on client', () => { localStorage.setItem(cart.localStorageCheckoutKey, 12345); const fetchCart = sinon.stub(cart.props.client.checkout, 'fetch').returns(Promise.resolve({id: 12345, lineItems: []})); return cart.fetchData().then((data) => { assert.deepEqual(data, {id: 12345, lineItems: []}); assert.calledOnce(fetchCart); fetchCart.restore(); }); }); it('resolves to null and removes localStorage key if checkout fetch fails', () => { localStorage.setItem(cart.localStorageCheckoutKey, 1); const fetchCart = sinon.stub(cart.props.client.checkout, 'fetch').returns(Promise.reject({errors: [{message: 'rejected.'}]})); return cart.fetchData().then((data) => { assert.deepEqual(data, null); assert.calledOnce(fetchCart); assert.equal(localStorage.getItem(cart.localStorageCheckoutKey), null); fetchCart.restore(); }); }); it('resolves to null and removes the localStorage key if checkout is completed', () => { localStorage.setItem(cart.localStorageCheckoutKey, 123); const fetchCart = sinon.stub(cart.props.client.checkout, 'fetch').returns(Promise.resolve({completedAt: '04-12-2018', lineItems: []})); return cart.fetchData().then((data) => { assert.equal(data, null); assert.calledOnce(fetchCart); assert.equal(localStorage.getItem(cart.localStorageCheckoutKey), null); fetchCart.restore(); }); }); it('calls sanitizeCheckout then updateCache with the new checkout', () => { localStorage.setItem(cart.localStorageCheckoutKey, 123); const checkout = {id: 1111, lineItems: [ {id: 1112, variant: null}, {id: 1113, variant: {id: 1114}}, {id: 1115, variant: null}, ]}; const sanitizedCheckout = {id: 1111, lineItems: [ {id: 1113, variant: {id: 1114}}, ]}; const fetchCart = sinon.stub(cart.props.client.checkout, 'fetch').resolves(checkout); const sanitizeCheckout = sinon.stub(cart, 'sanitizeCheckout').resolves(sanitizedCheckout); const updateCache = sinon.stub(cart, 'updateCache').resolves(); return cart.fetchData().then((data) => { assert.deepEqual(data, sanitizedCheckout); assert.calledOnce(fetchCart); assert.calledOnce(sanitizeCheckout); assert.calledOnce(updateCache); assert.calledWith(sanitizeCheckout, checkout); assert.calledWith(updateCache, sanitizedCheckout.lineItems); fetchCart.restore(); sanitizeCheckout.restore(); updateCache.restore(); }); }); }); describe('fetchMoneyFormat()', () => { it('calls fetchShopInfo on client', () => { localStorage.setItem(cart.localStorageCheckoutKey, 12345); const fetchMoneyFormat = sinon.stub(cart.props.client.shop, 'fetchInfo').returns(Promise.resolve({moneyFormat: '₿{{amount}}'})); return cart.fetchMoneyFormat().then((data) => { assert.deepEqual(data, '₿{{amount}}'); assert.calledOnce(fetchMoneyFormat); fetchMoneyFormat.restore(); }); }); }); describe('sanitizeCheckout()', () => { it('calls removeLineItems for all line items with deleted variants', () => { const checkout = {id: 1111, lineItems: [ {id: 1112, variant: null}, {id: 1113, variant: {id: 1114}}, {id: 1115, variant: null}, ]}; const sanitizedCheckout = {id: 1111, lineItems: [ {id: 1113, variant: {id: 1114}}, ]}; const removeLineItems = sinon.stub(cart.props.client.checkout, 'removeLineItems').resolves(sanitizedCheckout); return cart.sanitizeCheckout(checkout).then((data) => { assert.deepEqual(data, sanitizedCheckout); assert.calledOnce(removeLineItems); assert.calledWith(removeLineItems, 1111, [1112, 1115]); removeLineItems.restore(); }); }); }); describe('setQuantity()', () => { const node = { getAttribute: () => 1234, }; beforeEach(() => { cart.model = { lineItems: [{ id: 1234, quantity: 1, variant: { priceV2: { amount: '10.00', currencyCode: 'CAD', }, }, }], }; cart.updateItem = sinon.spy(); cart.cartItemTrackingInfo = sinon.spy(); }); it('calls updateItem', () => { cart.setQuantity(node, (n) => n + 1); assert.calledWith(cart.updateItem, 1234, 2); }); }); describe('updateItem()', () => { let updateLineItemsStub; let node; let quantityNode; let addClassToElementStub; const lineItemId = 123; const lineItemQuantity = 5; beforeEach(() => { node = document.createElement('div'); node.setAttribute('id', lineItemId); quantityNode = document.createElement('div'); quantityNode.setAttribute('class', cart.config.lineItem.classes.quantity); node.appendChild(quantityNode); document.body.appendChild(node); cart.model = { id: 123456, }; updateLineItemsStub = sinon.stub(cart.props.client.checkout, 'updateLineItems').returns(Promise.resolve({lineItems: [{id: lineItemId, quantity: lineItemQuantity}]})); addClassToElementStub = sinon.stub(elementClass, 'addClassToElement'); cart.view.render = sinon.spy(); cart.toggles[0].view.render = sinon.spy(); }); afterEach(() => { updateLineItemsStub.restore(); addClassToElementStub.restore(); document.body.removeChild(node); }); it('calls updateLineItem', () => { return cart.updateItem(lineItemId, lineItemQuantity).then(() => { assert.calledWith(updateLineItemsStub, 123456, [{id: lineItemId, quantity: lineItemQuantity}]); assert.calledOnce(cart.view.render); assert.calledOnce(cart.toggles[0].view.render); assert.deepEqual(cart.model, {lineItems: [{id: lineItemId, quantity: lineItemQuantity}]}); }); }); it('adds `is-loading` class to quantity element', () => { return cart.updateItem(lineItemId, lineItemQuantity).then(() => { assert.calledOnce(addClassToElementStub); assert.calledWith(addClassToElementStub, 'is-loading', quantityNode); }); }); }); describe('addVariantToCart', () => { const modelId = 135; const variantId = 1111; const quantity = 2; const variant = { id: variantId, }; const lineItem = {variantId, quantity}; let cartOpenStub; let setFocusStub; let addLineItemsStub; let checkoutCreateStub; let renderStub; let toggleRenderStub; let updateCacheStub; const mockCheckout = { id: 1001, lineItems: [{id: 1212, quantity: 4}], }; beforeEach(() => { cartOpenStub = sinon.stub(cart, 'open'); setFocusStub = sinon.stub(cart.view, 'setFocus'); addLineItemsStub = sinon.stub(cart.props.client.checkout, 'addLineItems').returns(Promise.resolve(mockCheckout)); checkoutCreateStub = sinon.stub(cart.props.client.checkout, 'create').returns(Promise.resolve(mockCheckout)); renderStub = sinon.stub(cart.view, 'render'); toggleRenderStub = sinon.stub(cart.toggles[0].view, 'render'); updateCacheStub = sinon.stub(cart, 'updateCache'); }); afterEach(() => { cartOpenStub.restore(); setFocusStub.restore(); addLineItemsStub.restore(); checkoutCreateStub.restore(); renderStub.restore(); toggleRenderStub.restore(); updateCacheStub.restore(); }); it('returns null if quantity parameter is 0', () => { assert.equal(cart.addVariantToCart(variant, 0), null); }); it('adds line item with quantity 1 if quantity parameter is not provided', () => { cart.model = { id: modelId, }; return cart.addVariantToCart(variant).then(() => { assert.calledWith(addLineItemsStub, modelId, [{variantId, quantity: 1}]); }); }); it('adds line item to checkout and returns the updated checkout if cart model exists', () => { cart.model = { id: modelId, }; return cart.addVariantToCart(variant, quantity).then((checkout) => { assert.notCalled(checkoutCreateStub); assert.calledOnce(addLineItemsStub); assert.calledWith(addLineItemsStub, modelId, [lineItem]); assert.deepEqual(checkout, mockCheckout); }); }); it('creates a checkout with line item and returns the updated checkout if cart model is null', () => { cart.model = null; return cart.addVariantToCart(variant, quantity).then((checkout) => { assert.calledOnce(checkoutCreateStub); assert.calledWith(checkoutCreateStub, {lineItems: [lineItem]}); assert.deepEqual(checkout, mockCheckout); }); }); it('calls open on cart if openCart parameter is not provided', () => { return cart.addVariantToCart(variant, quantity).then(() => { assert.calledOnce(cartOpenStub); }); }); it('calls open on cart if openCart parameter is true', () => { return cart.addVariantToCart(variant, quantity, true).then(() => { assert.calledOnce(cartOpenStub); }); }); it('does not call open on cart if openCart parameter is false', () => { return cart.addVariantToCart(variant, quantity, false).then(() => { assert.notCalled(cartOpenStub); }); }); }); describe('get formattedTotal', () => { const subtotalPriceAmount = '10.00'; const lineItemsSubtotalPriceAmount = '20.00'; describe('with model', () => { beforeEach(() => { cart.model = { subtotalPriceV2: { amount: subtotalPriceAmount, currentCode: 'CAD', }, lineItemsSubtotalPrice: { amount: lineItemsSubtotalPriceAmount, currencyCode: 'CAD', }, }; }); it('returns formatted subtotal price if contents discount field is true', () => { cart.config.cart.contents.discounts = true; assert.equal(cart.formattedTotal, `$${subtotalPriceAmount}`); }); it('returns formatted line items subtotal price if contents discount field is false', () => { cart.config.cart.contents.discounts = false; assert.equal(cart.formattedTotal, `$${lineItemsSubtotalPriceAmount}`); }); }); it('returns a formatted 0 price if model is null', () => { cart.model = null; assert.equal(cart.formattedTotal, '$0.00'); }); }); describe('get isEmpty', () => { it('returns true if cart model is null', () => { cart.model = null; assert.equal(cart.isEmpty, true); }); it('returns true if cart model line items array is empty', () => { cart.model = { lineItems: [], }; assert.equal(cart.isEmpty, true); }); it('returns false if cart model line items array is not empty', () => { cart.model = { lineItems: [{id: '123123'}], }; assert.equal(cart.isEmpty, false); }); }); describe('empty', () => { it('empties and rerenders the cart', () => { const removeLineItemsStub = sinon.stub(cart.props.client.checkout, 'removeLineItems').returns(Promise.resolve()); cart.view.render = sinon.spy(); cart.toggles[0].view.render = sinon.spy(); return cart.empty().then(() => { assert.calledOnce(removeLineItemsStub); assert.calledOnce(cart.view.render); assert.calledOnce(cart.toggles[0].view.render); }); }); }); describe('init', () => { let superInitStub; let fetchMoneyFormatStub; let toggleInitSpy; let mockToggle; const data = { key1: 'value1', key2: 'value2', }; beforeEach(() => { toggleInitSpy = sinon.spy(); mockToggle = { init: toggleInitSpy, destroy: sinon.spy(), }; superInitStub = sinon.stub(Component.prototype, 'init').resolves({}); fetchMoneyFormatStub = sinon.stub(cart, 'fetchMoneyFormat').resolves(); cart.toggles = [mockToggle]; }); afterEach(() => { superInitStub.restore(); fetchMoneyFormatStub.restore(); }); it('returns the cart instance', () => { return cart.init(data).then((returnValue) => { assert.deepEqual(cart, returnValue); }); }); it('calls fetchMoneyFormat when moneyFormat has not been set', () => { cart.moneyFormat = null; return cart.init().then(() => { assert.calledOnce(fetchMoneyFormatStub); }); }); it('does not call fetchMoneyFormat when moneyFormat has been set', () => { cart.moneyFormat = '₿{{amount}}'; return cart.init().then(() => { assert.notCalled(fetchMoneyFormatStub); }); }); it('calls init on super with the data parameter', () => { return cart.init(data).then(() => { assert.calledOnce(superInitStub); assert.calledWith(superInitStub, data); }); }); it('calls init on the toggle with an empty line item array if the cart model is null', () => { superInitStub.resolves({model: null}); return cart.init(data).then(() => { assert.calledOnce(toggleInitSpy); assert.calledWith(toggleInitSpy, {lineItems: []}); }); }); it('calls init on the toggle with the cart model`s line items', () => { const lineItems = [{id: 123, quantity: 5}]; superInitStub.resolves({model: {lineItems}}); return cart.init(data).then(() => { assert.calledOnce(toggleInitSpy); assert.calledWith(toggleInitSpy, {lineItems}); }); }); }); describe('DOMEvents', () => { it('binds closeCart to click on cart close', () => { cart.DOMEvents[`click ${cart.selectors.cart.close}`](); assert.calledOnce(closeCartSpy); }); describe('onQuantityIncrement bindings', () => { let quantityIncrementStub; beforeEach(() => { quantityIncrementStub = sinon.stub(cart, 'onQuantityIncrement'); }); afterEach(() => { quantityIncrementStub.restore(); }); it('binds onQuantityIncrement to click on quantity increment and passes a value of 1', () => { cart.DOMEvents[`click ${cart.selectors.lineItem.quantityIncrement}`](); assert.calledOnce(quantityIncrementStub); assert.calledWith(quantityIncrementStub, 1); }); it('bind onQuantityIncrement to click on quantity decrement and passes a value of -1', () => { cart.DOMEvents[`click ${cart.selectors.lineItem.quantityDecrement}`](); assert.calledOnce(quantityIncrementStub); assert.calledWith(quantityIncrementStub, -1); }); }); it('binds onCheckout to click on cart button', () => { const onCheckoutStub = sinon.stub(cart, 'onCheckout'); cart.DOMEvents[`click ${cart.selectors.cart.button}`](); assert.calledOnce(onCheckoutStub); onCheckoutStub.restore(); }); it('binds onQuantityBlur to blur on quantity input field', () => { const onQuantityBlurStub = sinon.stub(cart, 'onQuantityBlur'); cart.DOMEvents[`blur ${cart.selectors.lineItem.quantityInput}`](); assert.calledOnce(onQuantityBlurStub); onQuantityBlurStub.restore(); }); it('binds setNote to blur on cart note field', () => { const setNoteStub = sinon.stub(cart, 'setNote'); cart.DOMEvents[`blur ${cart.selectors.cart.note}`](); assert.calledOnce(setNoteStub); setNoteStub.restore(); }); }); describe('viewData()', () => { let viewData; describe('with model', () => { beforeEach(async () => { const lineItems = [ { id: 1234, quantity: 2, variant: { id: 1111, title: 'test variant', priceV2: { amount: '20.00', currencyCode: 'CAD', }, }, discountAllocations: [], }, ]; cart.model = { id: 1, lineItems, note: 'test cart note', subtotalPrice: '123.00', subtotalPriceV2: { amount: '130.00', currencyCode: 'USD', }, discountApplications: [], }; cart.lineItemCache = lineItems; viewData = cart.viewData; }); it('returns an object merged with model', () => { assert.equal(viewData.id, cart.model.id); assert.deepEqual(viewData.lineItems, cart.model.lineItems); assert.equal(viewData.subtotalPrice, cart.model.subtotalPrice); assert.equal(viewData.lineItemsSubtotalPrice, cart.model.lineItemsSubtotalPrice); }); it('returns an object with text', () => { assert.deepEqual(viewData.text, cart.options.text); }); it('returns an object with classes', () => { assert.deepEqual(viewData.classes, cart.classes); }); it('returns an object with lineItemsHtml', () => { assert.equal(viewData.lineItemsHtml, cart.lineItemsHtml); }); it('returns an object with isEmpty', () => { assert.equal(viewData.isEmpty, cart.isEmpty); }); it('returns an object with formatted total', () => { assert.equal(viewData.formattedTotal, cart.formattedTotal); }); it('returns an object with contents', () => { assert.deepEqual(viewData.contents, cart.options.contents); }); it('returns an object with cart note', () => { assert.equal(viewData.cartNote, cart.cartNote); }); }); describe('without model', () => { beforeEach(() => { cart.model = null; viewData = cart.viewData; }); it('returns an object with text', () => { assert.deepEqual(viewData.text, cart.options.text); }); it('returns an object with classes', () => { assert.deepEqual(viewData.classes, cart.classes); }); it('returns an object with lineItemsHtml', () => { assert.equal(viewData.lineItemsHtml, cart.lineItemsHtml); }); it('returns an object with isEmpty', () => { assert.equal(viewData.isEmpty, cart.isEmpty); }); it('returns an object with formatted total', () => { assert.equal(viewData.formattedTotal, cart.formattedTotal); }); it('returns an object with contents', () => { assert.deepEqual(viewData.contents, cart.options.contents); }); it('returns an object with cart note', () => { assert.equal(viewData.cartNote, cart.cartNote); }); }); }); describe('onCheckout()', () => { let openCheckoutStub; let userEventStub; beforeEach(() => { openCheckoutStub = sinon.stub(cart.checkout, 'open'); userEventStub = sinon.stub(cart, '_userEvent'); cart.onCheckout(); }); afterEach(() => { openCheckoutStub.restore(); userEventStub.restore(); }); it('triggers open checkout user event', () => { assert.calledOnce(userEventStub); assert.calledWith(userEventStub, 'openCheckout'); }); it('tracks open checkout', () => { assert.calledOnce(trackSpy); assert.calledWith(trackSpy, 'Open cart checkout', {}); }); it('open checkout', () => { assert.calledOnce(openCheckoutStub); assert.calledWith(openCheckoutStub, cart.model.webUrl); }); }); describe('get cartNote', () => { it('returns null if the cart model doesn`t exist', () => { cart.model = null; assert.equal(cart.cartNote, null); }); it('returns the note from the cart model', () => { const note = 'test cart note'; cart.model.note = note; assert.equal(cart.cartNote, cart.model.note); }); }); describe('setNote()', () => { let updateAttributesStub; const mockCheckout = { lineItems: [ {id: '1'}, {id: '2'}, ], }; const note = 'test cart note'; const event = { target: { value: note, }, }; beforeEach(() => { updateAttributesStub = sinon.stub(cart.props.client.checkout, 'updateAttributes').resolves(mockCheckout); }); afterEach(() => { updateAttributesStub.restore(); }); it('calls updateAttributes on the cart', async () => { await cart.setNote(event); assert.calledOnce(updateAttributesStub); assert.calledWith(updateAttributesStub, cart.model.id, {note}); }); it('sets the cart model to the checkout returned from the client', async () => { await cart.setNote(event); assert.equal(cart.model, mockCheckout); }); }); describe('get cartDiscounts', () => { it('returns an empty array is cart model is null', () => { cart.model = null; assert.equal(cart.cartDiscounts.length, 0); }); it('return an empty array if no discount applications exist', () => { cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [], }; assert.equal(cart.cartDiscounts.length, 0); }); it('returns an array of discount details if discount applications exist', () => { cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [ { title: 'BOGO', targetSelection: 'ALL', value: { percentage: '20', }, }, { title: 'BOGO', targetSelection: 'ALL', value: { amount: '2.00', currencyCode: 'CAD', }, }, ], }; assert.equal(cart.cartDiscounts.length, 2); }); it('does not return discount details for a discount application with target selection `ENTITLED`', () => { cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [ { title: 'BOGO', targetSelection: 'ENTITLED', value: { amount: '2.00', currencyCode: 'CAD', }, }, ], }; assert.equal(cart.cartDiscounts.length, 0); }); it('does not return discount details for a discount application with target selection `EXPLICIT`', () => { cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [ { title: 'BOGO', targetSelection: 'EXPLICIT', value: { amount: '2.00', currencyCode: 'CAD', }, }, ], }; assert.equal(cart.cartDiscounts.length, 0); }); it('returns discount details for amount based discount applications with target selection `ALL`', () => { const discountTitle = 'BOGO'; const discountAmount = '2.00'; cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [ { title: discountTitle, targetSelection: 'ALL', value: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }; const discounts = cart.cartDiscounts; assert.equal(discounts.length, 1); assert.deepEqual(discounts[0], { text: discountTitle, amount: `-$${discountAmount}`, }); }); it('returns discount details for percentage based discount applications with target selection `ALL`', () => { const discountTitle = 'BOGO'; const discountPercentage = '20'; const lineItemSubtotal = '20.0'; cart.model = { lineItemsSubtotalPrice: { amount: lineItemSubtotal, currencyCode: 'CAD', }, discountApplications: [ { title: discountTitle, targetSelection: 'ALL', value: { percentage: discountPercentage, }, }, ], }; const discounts = cart.cartDiscounts; assert.equal(discounts.length, 1); assert.deepEqual(discounts[0], { text: discountTitle, amount: `-$${discountPercentage / 100 * lineItemSubtotal}.00`, }); }); it('returns discount code as discount title when target selection is `ALL` and discount title does not exists', () => { const discountCode = 'BOGO'; const discountAmount = '2.00'; cart.model = { lineItemsSubtotalPrice: { amount: '20.0', currencyCode: 'CAD', }, discountApplications: [ { code: discountCode, targetSelection: 'ALL', value: { amount: discountAmount, currencyCode: 'CAD', }, }, ], }; const discounts = cart.cartDiscounts; assert.equal(discounts.length, 1); assert.equal(discounts[0].text, discountCode); }); }); describe('cartItemTrackingInfo', () => { it('returns tracking info for cart item', () => { const item = { title: 'Test Sunglasses', quantity: 2, variant: { id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzE1MjQzMDkwNDg', title: 'Black shades', priceV2: { amount: '50.0', }, product: { id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ2Nzc3MjkxOTg2NA', }, }, }; const trackingInfo = cart.cartItemTrackingInfo(item, '5'); assert.deepEqual(trackingInfo, { id: item.variant.id, variantName: item.variant.title, productId: item.variant.product.id, name: item.title, price: item.variant.priceV2.amount, prevQuantity: item.quantity, quantity: 5, sku: null, }); }); }); describe('close()', () => { let viewRenderSpy; let removeTrapFocusStub; beforeEach(() => { viewRenderSpy = sinon.spy(); cart.view.render = viewRenderSpy; removeTrapFocusStub = sinon.stub(focusUtils, 'removeTrapFocus'); cart.isVisible = true; cart.close(); }); afterEach(() => { removeTrapFocusStub.restore(); }); it('sets isVisible to false', () => { assert.equal(cart.isVisible, false); }); it('calls the view`s render function', () => { assert.calledOnce(viewRenderSpy); }); it('calls removeTrapFocus with the view wrapper', () => { assert.calledOnce(removeTrapFocusStub); assert.calledWith(removeTrapFocusStub.firstCall, cart.view.wrapper); }); }); });