Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions force-app/main/default/lwc/cartSummary/__tests__/cartSummary.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1169,4 +1169,248 @@ describe('Express payment URL construction', () => {
consoleSpy.mockRestore();
});
});

describe('Coupon input functionality', () => {
it('should show coupon input when cart has items and feature flag is enabled', () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;
element.configuration = { couponInput: { enabled: true } };

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();
});
});

it('should not show coupon input when cart has no items', () => {
const cartWithoutItems = {
...mockCartSummary,
items: [],
};
element.cartSummary = cartWithoutItems;

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).toBeNull();
});
});

it('should not show coupon input when items property is undefined', () => {
const cartWithoutItems = {
...mockCartSummary,
items: undefined,
};
element.cartSummary = cartWithoutItems;

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).toBeNull();
});
});

it('should not show coupon input when cart summary is empty', () => {
element.cartSummary = {};

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).toBeNull();
});
});

it('should handle applycoupon event and dispatch cartapplycoupon event', () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;
element.configuration = { couponInput: { enabled: true } };

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();

const eventHandler = jest.fn();
element.addEventListener('cartapplycoupon', eventHandler);

// Simulate applycoupon event from coupon input
const applyCouponEvent = new CustomEvent('applycoupon', {
detail: { couponCode: 'SAVE10' },
bubbles: true,
composed: true,
});
couponInput.dispatchEvent(applyCouponEvent);

expect(eventHandler).toHaveBeenCalledTimes(1);
expect(eventHandler.mock.calls[0][0].detail).toEqual({ couponCode: 'SAVE10' });
});
});

it('should bubble up cartapplycoupon event with correct coupon code', () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;
element.configuration = { couponInput: { enabled: true } };

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();

const eventHandler = jest.fn();
element.addEventListener('cartapplycoupon', eventHandler);

// Dispatch applycoupon event from coupon input
const applyCouponEvent = new CustomEvent('applycoupon', {
detail: { couponCode: 'DISCOUNT20' },
bubbles: true,
composed: true,
});
couponInput.dispatchEvent(applyCouponEvent);

expect(eventHandler).toHaveBeenCalledTimes(1);
expect(eventHandler.mock.calls[0][0].detail.couponCode).toBe('DISCOUNT20');
expect(eventHandler.mock.calls[0][0].bubbles).toBe(true);
expect(eventHandler.mock.calls[0][0].composed).toBe(true);
});
});

it('should disable apply button when coupon code is empty', async () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;
element.configuration = { couponInput: { enabled: true } };

await Promise.resolve();

const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();

// Set empty coupon code
couponInput.couponCode = '';

await Promise.resolve();
const applyButton = couponInput.querySelector('button');
expect(applyButton.disabled).toBe(true);
});

it('should disable apply button when coupon code is only whitespace', async () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;
element.configuration = { couponInput: { enabled: true } };

await Promise.resolve();

const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();

// Set whitespace-only coupon code
couponInput.couponCode = ' ';

await Promise.resolve();
const applyButton = couponInput.querySelector('button');
expect(applyButton.disabled).toBe(true);
});

describe('Coupon input feature', () => {
it('should show coupon input when cart has items', () => {
const cartWithItems = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
element.cartSummary = cartWithItems;

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();
});
});

it('should not show coupon input when cart is empty', () => {
const cartWithoutItems = {
...mockCartSummary,
items: [],
};
element.cartSummary = cartWithoutItems;

return Promise.resolve().then(() => {
const couponInput = element.querySelector('c-coupon-input');
expect(couponInput).toBeNull();
});
});
});
});

describe('Coupon feature flag (isCouponFeatureEnabled)', () => {
it('should show coupon input when feature flag is true', async () => {
const newElement = createElement('c-cart-summary', {
is: CartSummary,
});
newElement.cartSummary = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: true },
};
document.body.appendChild(newElement);

await Promise.resolve();

const couponInput = newElement.querySelector('c-coupon-input');
expect(couponInput).not.toBeNull();

document.body.removeChild(newElement);
});

it('should hide coupon input when feature flag is false', async () => {
const newElement = createElement('c-cart-summary', {
is: CartSummary,
});
newElement.cartSummary = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
flags: { isCouponFeatureEnabled: false },
};
document.body.appendChild(newElement);

await Promise.resolve();

const couponInput = newElement.querySelector('c-coupon-input');
expect(couponInput).toBeNull();

document.body.removeChild(newElement);
});

it('should hide coupon input when feature flag is undefined (defaults to false)', async () => {
const newElement = createElement('c-cart-summary', {
is: CartSummary,
});
newElement.cartSummary = {
...mockCartSummary,
items: [{ name: 'Product A', quantity: 1, itemSubtotal: 50.0 }],
// No flags property
};
document.body.appendChild(newElement);

await Promise.resolve();

const couponInput = newElement.querySelector('c-coupon-input');
expect(couponInput).toBeNull();

document.body.removeChild(newElement);
});
});
});
8 changes: 8 additions & 0 deletions force-app/main/default/lwc/cartSummary/cartSummary.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
configuration={configuration}
is-cart-summary></c-summary-details>

<template if:true={shouldShowCouponInput}>
<div class="coupon-input-wrapper slds-p-horizontal_medium slds-p-vertical_small">
<c-coupon-input
configuration={configuration}
onapplycoupon={handleApplyCoupon}></c-coupon-input>
</div>
</template>

<div class={loadingContainerClass}>
<template if:true={shouldWaitForExpressPayment}>
<template if:false={isExpressLoaded}>
Expand Down
28 changes: 28 additions & 0 deletions force-app/main/default/lwc/cartSummary/cartSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,32 @@ export default class CartSummary extends LightningElement {
console.warn(`Failed to send basket data postMessage (Component ${this._instanceId}):`, error);
}
}

/**
* @description Determines if the coupon input should be displayed
* @returns {boolean} True if cart summary has items and feature flag is enabled
*/
get shouldShowCouponInput() {
const hasItems = this._cartSummary && this._cartSummary.items && this._cartSummary.items.length > 0;
const isCouponFeatureShown = this._cartSummary?.flags?.isCouponFeatureEnabled ?? false;
return hasItems && isCouponFeatureShown;
}

/**
* @description Handles the applycoupon event from the coupon input component
* @param {CustomEvent} event - The applycoupon event containing coupon code
*/
handleApplyCoupon(event) {
const { couponCode } = event.detail;

// Dispatch custom event to parent/container to handle coupon application
const applyCouponEvent = new CustomEvent('cartapplycoupon', {
detail: {
couponCode,
},
bubbles: true,
composed: true,
});
this.dispatchEvent(applyCouponEvent);
}
}
30 changes: 8 additions & 22 deletions force-app/main/default/lwc/cartSummary/labelUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,17 @@

/*
* @description Label functions and helper utilities for cartSummary
* Uses shared labelService for translation logic
*/

import { getTranslatedLabel } from 'c/labelService';
import { LABEL_DATA } from './labels';

/**
* Gets a translated label for the given key and locale
* @param {string} labelKey - The key to look up in the label data
* @param {string} locale - The locale code (e.g., 'en-US', 'es-ES', 'fr-FR')
* @returns {string} The translated label or fallback value
*/
function getTranslatedLabel(labelKey, locale = 'en_US') {
const label = LABEL_DATA[labelKey];
if (label && label[locale]) {
return label[locale];
}
if (label && label.en_US) {
return label.en_US;
}
return labelKey;
}

// Export individual label functions
export const cartSummaryRegionLabel = (locale) => getTranslatedLabel('cartSummaryRegionLabel', locale);
export const loadingSpinnerAltText = (locale) => getTranslatedLabel('loadingSpinnerAltText', locale);
export const checkoutButtonLabel = (locale) => getTranslatedLabel('checkoutButtonLabel', locale);
export const checkoutButtonAssistiveText = (locale) => getTranslatedLabel('checkoutButtonAssistiveText', locale);
export const cartSummaryRegionLabel = (locale) => getTranslatedLabel('cartSummaryRegionLabel', LABEL_DATA, locale);
export const loadingSpinnerAltText = (locale) => getTranslatedLabel('loadingSpinnerAltText', LABEL_DATA, locale);
export const checkoutButtonLabel = (locale) => getTranslatedLabel('checkoutButtonLabel', LABEL_DATA, locale);
export const checkoutButtonAssistiveText = (locale) =>
getTranslatedLabel('checkoutButtonAssistiveText', LABEL_DATA, locale);
export const checkoutNotAvailableAssistiveText = (locale) =>
getTranslatedLabel('checkoutNotAvailableAssistiveText', locale);
getTranslatedLabel('checkoutNotAvailableAssistiveText', LABEL_DATA, locale);
36 changes: 10 additions & 26 deletions force-app/main/default/lwc/commerceHeader/labelUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,18 @@

/*
* @description Group all Custom Labels for commerceHeader in one place
* Uses shared labelService for translation logic
*/

import { getTranslatedLabel } from 'c/labelService';
import { LABEL_DATA } from './labels';

// Helper function to get translated label using shared utility
/**
* Gets a translated label for the given key and locale
* @param {string} labelKey - The key to look up in the label data
* @param {string} locale - The locale code (e.g., 'en-US', 'es-ES', 'fr-FR')
* @returns {string} The translated label or fallback value
*/
function getTranslatedLabel(labelKey, locale = 'en_US') {
const label = LABEL_DATA[labelKey];
if (label && label[locale]) {
return label[locale];
}
if (label && label.en_US) {
return label.en_US;
}
return labelKey;
}

// Export individual label functions
export const menu = (locale) => getTranslatedLabel('menu', locale);
export const requestTranscript = (locale) => getTranslatedLabel('requestTranscript', locale);
export const endChat = (locale) => getTranslatedLabel('endChat', locale);
export const minimize = (locale) => getTranslatedLabel('minimize', locale);
export const minimizeAssistive = (locale) => getTranslatedLabel('minimizeAssistive', locale);
export const logoAlt = (locale) => getTranslatedLabel('logoAlt', locale);
export const closeButtonAssistiveText = (locale) => getTranslatedLabel('closeButtonAssistiveText', locale);
export const defaultHeaderText = (locale) => getTranslatedLabel('defaultHeaderText', locale);
export const menu = (locale) => getTranslatedLabel('menu', LABEL_DATA, locale);
export const requestTranscript = (locale) => getTranslatedLabel('requestTranscript', LABEL_DATA, locale);
export const endChat = (locale) => getTranslatedLabel('endChat', LABEL_DATA, locale);
export const minimize = (locale) => getTranslatedLabel('minimize', LABEL_DATA, locale);
export const minimizeAssistive = (locale) => getTranslatedLabel('minimizeAssistive', LABEL_DATA, locale);
export const logoAlt = (locale) => getTranslatedLabel('logoAlt', LABEL_DATA, locale);
export const closeButtonAssistiveText = (locale) => getTranslatedLabel('closeButtonAssistiveText', LABEL_DATA, locale);
export const defaultHeaderText = (locale) => getTranslatedLabel('defaultHeaderText', LABEL_DATA, locale);
Loading