diff --git a/spec/api/cart.spec.js b/spec/api/cart.spec.js index 71919a4..8ba7508 100644 --- a/spec/api/cart.spec.js +++ b/spec/api/cart.spec.js @@ -81,3 +81,50 @@ describe('Cart Api Class', () => { expect(cart.makeRequest).toHaveBeenCalled(); }); }); + +describe('getShippingQuotes', () => { + let cart; + + beforeEach(() => { + cart = new CartApi(); + cart.remoteRequest = jest.fn(); + }); + + // Mirrors: utils.api.cart.getShippingQuotes(params, 'cart/shipping-quotes', callback) + test('3-arg call (no requestOptions): template and params are forwarded', () => { + const callback = jest.fn(); + + cart.getShippingQuotes({ zip: '10001' }, 'cart/shipping-quotes', callback); + + expect(cart.remoteRequest).toHaveBeenCalledWith( + '/shipping-quote', + 'GET', + expect.objectContaining({ params: { zip: '10001' }, template: 'cart/shipping-quotes' }), + callback, + ); + }); + + // Mirrors: utils.api.cart.getShippingQuotes(params, 'cart/shipping-quotes', callback, { baseUrl: secureBaseUrl }) + test('4-arg call (with baseUrl): baseUrl is forwarded and template is not overwritten', () => { + const callback = jest.fn(); + + cart.getShippingQuotes({ zip: '10001' }, 'cart/shipping-quotes', callback, { baseUrl: 'https://store.example.com/es' }); + + const [, , passedOptions] = cart.remoteRequest.mock.calls[0]; + expect(passedOptions.baseUrl).toBe('https://store.example.com/es'); + expect(passedOptions.template).toBe('cart/shipping-quotes'); + expect(passedOptions.params).toEqual({ zip: '10001' }); + }); + + // Regression: baseUrl must not be silently dropped when renderWith is omitted + test('3-arg call without renderWith (params, callback, requestOptions): baseUrl is forwarded', () => { + const callback = jest.fn(); + + cart.getShippingQuotes({ zip: '10001' }, callback, { baseUrl: 'https://store.example.com/es' }); + + const [, , passedOptions, actualCallback] = cart.remoteRequest.mock.calls[0]; + expect(actualCallback).toBe(callback); + expect(passedOptions.baseUrl).toBe('https://store.example.com/es'); + expect(passedOptions.params).toEqual({ zip: '10001' }); + }); +}); diff --git a/spec/api/product-attributes.spec.js b/spec/api/product-attributes.spec.js new file mode 100644 index 0000000..d731260 --- /dev/null +++ b/spec/api/product-attributes.spec.js @@ -0,0 +1,103 @@ +import Base from '../../src/api/base'; +import ProductAttributes from '../../src/api/product-attributes'; + +jest.mock('../../src/api/base'); + +describe('ProductAttributes', () => { + let productAttributes; + + beforeEach(() => { + Base.prototype.remoteRequest = jest.fn(); + productAttributes = new ProductAttributes(); + }); + + describe('optionChange', () => { + // Mirrors: optionChange(simpleProductId, $form.serialize(), (err, response) => { ... }) + test('3-arg call (no template, no baseUrl): callback is invoked with server response', () => { + const fakeResponse = { data: { stock: 5 } }; + Base.prototype.remoteRequest = jest.fn((url, method, options, internalCb) => { + internalCb(null, fakeResponse); + }); + productAttributes = new ProductAttributes(); + const callback = jest.fn(); + + productAttributes.optionChange(99, 'qty=1', callback); + + expect(callback).toHaveBeenCalledWith(null, fakeResponse); + expect(productAttributes.remoteRequest).toHaveBeenCalledWith( + '/product-attributes/99', + 'POST', + expect.objectContaining({ template: null }), + expect.any(Function), + ); + }); + + // Mirrors: optionChange(productId, $form.serialize(), 'products/bulk-discount-rates', (err, response) => { ... }, { baseUrl: this.context.secureBaseUrl }) + test('5-arg call (template + baseUrl): callback is invoked and baseUrl is forwarded', () => { + const fakeResponse = { data: { price: '$9.99' }, content: '
' }; + Base.prototype.remoteRequest = jest.fn((url, method, options, internalCb) => { + internalCb(null, fakeResponse); + }); + productAttributes = new ProductAttributes(); + const callback = jest.fn(); + + productAttributes.optionChange(99, 'qty=1', 'products/bulk-discount-rates', callback, { baseUrl: 'https://store.example.com/es' }); + + expect(callback).toHaveBeenCalledWith(null, fakeResponse); + const [, , passedOptions] = productAttributes.remoteRequest.mock.calls[0]; + expect(passedOptions.template).toBe('products/bulk-discount-rates'); + expect(passedOptions.baseUrl).toBe('https://store.example.com/es'); + }); + }); + + describe('configureInCart', () => { + test('template + baseUrl stay top-level (flat merge)', () => { + const callback = jest.fn(); + + productAttributes.configureInCart( + 'item-abc', + { template: 'cart/modals/configure-product' }, + callback, + { baseUrl: 'https://store.example.com/es' }, + ); + + expect(productAttributes.remoteRequest).toHaveBeenCalledWith( + '/configure-options/item-abc', + 'GET', + expect.objectContaining({ + template: 'cart/modals/configure-product', + baseUrl: 'https://store.example.com/es', + }), + expect.any(Function), + ); + }); + + test('flat merge: extra keys from first arg and baseUrl from requestOptions', () => { + const callback = jest.fn(); + + productAttributes.configureInCart('item-abc', { qty: 1 }, callback, { baseUrl: 'https://store.example.com/es' }); + + expect(productAttributes.remoteRequest).toHaveBeenCalledWith( + '/configure-options/item-abc', + 'GET', + expect.objectContaining({ qty: 1, baseUrl: 'https://store.example.com/es' }), + expect.any(Function), + ); + }); + + test('requestOptions.baseUrl wins when first arg also has baseUrl', () => { + const callback = jest.fn(); + + productAttributes.configureInCart( + 'item-abc', + { template: 'cart/modals/configure-product', baseUrl: 'https://wrong.example.com' }, + callback, + { baseUrl: 'https://store.example.com/es' }, + ); + + const [, , passedOptions] = productAttributes.remoteRequest.mock.calls[0]; + expect(passedOptions.baseUrl).toBe('https://store.example.com/es'); + expect(passedOptions.template).toBe('cart/modals/configure-product'); + }); + }); +}); diff --git a/src/api/cart.js b/src/api/cart.js index d0b18a9..34cec6d 100644 --- a/src/api/cart.js +++ b/src/api/cart.js @@ -293,19 +293,25 @@ export default class extends Base { * @param {Object} params * @param {String|Array|Object} renderWith * @param {Function} callback + * @param {Object} [requestOptions] */ - getShippingQuotes(params, renderWith, callback) { - const options = { - params, - }; + getShippingQuotes(params, renderWith, callback, requestOptions = {}) { let callbackArg = callback; let renderWithArg = renderWith; + let requestOptionsArg = requestOptions; if (typeof callbackArg !== 'function') { + // renderWith was omitted — shift remaining args: callback → requestOptions + requestOptionsArg = callbackArg || {}; callbackArg = renderWithArg; renderWithArg = null; } + const options = { + ...requestOptionsArg, + params, + }; + if (renderWithArg) { options.template = renderWithArg; } diff --git a/src/api/product-attributes.js b/src/api/product-attributes.js index a4cc122..37f951c 100644 --- a/src/api/product-attributes.js +++ b/src/api/product-attributes.js @@ -18,18 +18,23 @@ export default class extends Base { /** * @param {Number} productId * @param {Object} params - * @param callback + * @param {String|null} [template] + * @param {Function} callback + * @param {Object} [requestOptions] */ - optionChange(productId, params, template = null, callback) { + optionChange(productId, params, template = null, callback, requestOptions = {}) { let templateArg = template; let callbackArg = callback; + let requestOptionsArg = requestOptions; if (typeof templateArg === 'function') { + // template was omitted — shift remaining args: callback → requestOptions + requestOptionsArg = callbackArg || {}; callbackArg = templateArg; templateArg = null; } - this.remoteRequest(this.endpoint + productId, 'POST', { params: parse(params), template: templateArg }, (err, response) => { + this.remoteRequest(this.endpoint + productId, 'POST', { ...requestOptionsArg, params: parse(params), template: templateArg }, (err, response) => { const emitData = { err, response, @@ -43,10 +48,11 @@ export default class extends Base { /** * @param {Number} itemId * @param {Object} params - * @param callback + * @param {Function} callback + * @param {Object} [requestOptions] */ - configureInCart(itemId, params, callback) { - this.remoteRequest(this.inCartEndpoint + itemId, 'GET', params, (err, response) => { + configureInCart(itemId, params, callback, requestOptions = {}) { + this.remoteRequest(this.inCartEndpoint + itemId, 'GET', { ...params, ...requestOptions }, (err, response) => { callback(err, response); }); }