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
24 changes: 24 additions & 0 deletions spec/components/ProductCard/productCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import ProductCard from '../../../src/components/ProductCard';
import CioPlp from '../../../src/components/CioPlp';
import { DEMO_API_KEY } from '../../../src/constants';
import testItem from '../../local_examples/item.json';
import testItemWithSalePrice from '../../local_examples/itemWithSalePrice.json';
import { transformResultItem } from '../../../src/utils/transformers';
import { copyItemWithNewSalePrice } from '../../test-utils';

describe('Testing Component: ProductCard', () => {
test('Should throw error if used outside the CioPlp', () => {
Expand Down Expand Up @@ -167,4 +169,26 @@ describe('Testing Component: ProductCard', () => {

screen.getByText('My Rendered Price: $90.00');
});

test('Should render sale price when salePrice is present and valid', () => {
render(
<CioPlp apiKey={DEMO_API_KEY}>
<ProductCard item={transformResultItem(testItemWithSalePrice)} />
</CioPlp>,
);

screen.getByText('$21.00');
});

test('Should not render sale price when salePrice is undefined', () => {
render(
<CioPlp apiKey={DEMO_API_KEY}>
<ProductCard
item={transformResultItem(copyItemWithNewSalePrice(testItemWithSalePrice, undefined))}
/>
</CioPlp>,
);

expect(screen.queryByTestId('cio-sale-price')).toBeNull();
});
});
94 changes: 67 additions & 27 deletions spec/hooks/useProductInfo/useProductInfo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useProductInfo from '../../../src/hooks/useProduct';
import { transformResultItem } from '../../../src/utils/transformers';
import mockItem from '../../local_examples/item.json';
import mockItemWithSalePrice from '../../local_examples/itemWithSalePrice.json';
import { renderHookWithCioPlp } from '../../test-utils';
import { renderHookWithCioPlp, copyItemWithNewSalePrice } from '../../test-utils';

describe('Testing Hook: useProductInfo', () => {
beforeEach(() => {
Expand Down Expand Up @@ -41,32 +41,7 @@ describe('Testing Hook: useProductInfo', () => {
});
});

describe.each([
{
item: transformedItem,
itemDescription: 'a standard item',
},
{
item: transformedItemWithSalePrice,
itemDescription: 'an item on sale',
},
])('With $itemDescription', ({ item }) => {
it.each([
['itemId', item.itemId],
['itemName', item.itemName],
['itemImageUrl', item.imageUrl],
['itemUrl', item.url],
['itemPrice', item.data.price],
['salePrice', item.data.sale_price],
])('Should return the correct value for "%s"', async (property, expectedValue) => {
const { result } = renderHookWithCioPlp(() => useProductInfo({ item }));
await waitFor(() => {
expect(result.current[property]).toEqual(expectedValue);
});
});
});

it('Should return correctly after different variation is selected', async () => {
it('Should return correctly after a different variation is selected', async () => {
const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem }));

await waitFor(() => {
Expand Down Expand Up @@ -120,4 +95,69 @@ describe('Testing Hook: useProductInfo', () => {
expect(itemSalePrice).toBeUndefined();
});
});

describe.each([
{
item: transformedItem,
itemDescription: 'a standard item',
},
{
item: transformedItemWithSalePrice,
itemDescription: 'an item on sale',
},
])('With $itemDescription', ({ item }) => {
it.each([
['itemId', item.itemId],
['itemName', item.itemName],
['itemImageUrl', item.imageUrl],
['itemUrl', item.url],
['itemPrice', item.data.price],
['salePrice', item.data.sale_price],
])('Should return the correct value for "%s"', async (property, expectedValue) => {
const { result } = renderHookWithCioPlp(() => useProductInfo({ item }));
await waitFor(() => {
expect(result.current[property]).toEqual(expectedValue);
});
});
});

describe('Testing sale price handling logic', () => {
describe.each([
{
desc: 'undefined salePrice',
salePrice: undefined,
expected: undefined,
},
{
desc: 'negative salePrice',
salePrice: -5,
expected: undefined,
},
{
desc: 'salePrice greater than or equal to price',
salePrice: Infinity,
expected: undefined,
},
{
desc: 'valid salePrice (positive and less than price)',
salePrice: 1,
expected: 1,
},
{
desc: 'zero salePrice',
salePrice: 0,
expected: undefined,
},
])('When $desc', ({ salePrice, expected }) => {
it(`Should return ${expected === undefined ? 'undefined' : expected} for salePrice`, async () => {
const item = transformResultItem(
copyItemWithNewSalePrice(mockItemWithSalePrice, salePrice),
);
const { result } = renderHookWithCioPlp(() => useProductInfo({ item }));
await waitFor(() => {
expect(result.current.salePrice).toEqual(expected);
});
});
});
});
});
21 changes: 21 additions & 0 deletions spec/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,32 @@ const delay = (ms) =>
const getAttribute = (container) => (attribute) =>
container.querySelector(`[${attribute}]`)?.getAttribute(attribute);

function copyItemWithNewSalePrice(item, mockSalePrice) {
const itemCopy = { ...item };
if (itemCopy.variations && Array.isArray(itemCopy.variations)) {
itemCopy.variations = itemCopy.variations.map((variation) => ({
...variation,
data: {
...variation.data,
sale_price: mockSalePrice,
},
}));
}
return {
...itemCopy,
data: {
...itemCopy.data,
sale_price: mockSalePrice,
},
};
}

export {
customRender as renderWithCioPlp,
customRenderHook as renderHookWithCioPlp,
mockConstructorIOClient,
CioPlpWrapper,
delay,
getAttribute,
copyItemWithNewSalePrice,
};
14 changes: 9 additions & 5 deletions src/components/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import { useCioPlpContext } from '../../hooks/useCioPlpContext';
import { useOnAddToCart, useOnProductCardClick } from '../../hooks/callbacks';
import { CnstrcData, IncludeRenderProps, Item, ProductInfoObject } from '../../types';
Expand Down Expand Up @@ -56,7 +56,8 @@ export default function ProductCard(props: ProductCardProps) {
const { item, children } = props;
const state = useCioPlpContext();
const productInfo = useProductInfo({ item });
const { productSwatch, itemName, itemPrice, itemImageUrl, itemUrl, salePrice } = productInfo;
const { productSwatch, itemName, itemPrice, itemImageUrl, itemUrl, salePrice, hasSalePrice } =
productInfo;

if (!state) {
throw new Error('This component is meant to be used within the CioPlp provider.');
Expand All @@ -70,7 +71,6 @@ export default function ProductCard(props: ProductCardProps) {
const onAddToCart = useOnAddToCart(client, state.callbacks.onAddToCart);
const { formatPrice } = state.formatters;
const onClick = useOnProductCardClick(client, state.callbacks.onProductCardClick);
const hasSalesPrice = useMemo(() => !!(salePrice && Number(salePrice) >= 0), [salePrice]);

const cnstrcData = getProductCardCnstrcDataAttributes(productInfo);

Expand All @@ -97,12 +97,16 @@ export default function ProductCard(props: ProductCardProps) {

<div className='cio-content'>
<div className='cio-item-prices-container'>
{hasSalesPrice && <div className='cio-item-price'>{formatPrice(salePrice)}</div>}
{hasSalePrice && (
<div className='cio-item-price' id='cio-sale-price'>
{formatPrice(salePrice)}
</div>
)}
{Number(itemPrice) >= 0 && (
<div
className={concatStyles(
'cio-item-price',
hasSalesPrice && 'cio-item-price-strikethrough',
hasSalePrice && 'cio-item-price-strikethrough',
)}>
{formatPrice(itemPrice)}
</div>
Expand Down
12 changes: 10 additions & 2 deletions src/hooks/useProduct.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import useProductSwatch from './useProductSwatch';
import { useCioPlpContext } from './useCioPlpContext';
import { UseProductInfo } from '../types';
import { tryCatchify } from '../utils';
import { tryCatchify, isValidSalePrice } from '../utils';
import {
getPrice as defaultGetPrice,
getSalePrice as defaultGetSalePrice,
Expand All @@ -20,12 +20,19 @@ const useProductInfo: UseProductInfo = ({ item }) => {

const itemName = productSwatch?.selectedVariation?.itemName || item.itemName;
const itemPrice = productSwatch?.selectedVariation?.price || getPrice(item);
const salePrice = productSwatch?.selectedVariation?.salePrice || getSalePrice(item);
const itemImageUrl = productSwatch?.selectedVariation?.imageUrl || item.imageUrl;
const itemUrl = productSwatch?.selectedVariation?.url || item.url;
const variationId = productSwatch?.selectedVariation?.variationId;
const { itemId } = item;

let salePrice = productSwatch?.selectedVariation?.salePrice || getSalePrice(item);
let hasSalePrice = true;

if (!isValidSalePrice(salePrice, itemPrice)) {
salePrice = undefined;
hasSalePrice = false;
}

return {
productSwatch,
itemName,
Expand All @@ -35,6 +42,7 @@ const useProductInfo: UseProductInfo = ({ item }) => {
variationId,
itemId,
salePrice,
hasSalePrice,
};
};

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export interface ProductInfoObject {
itemUrl?: string;
itemImageUrl?: string;
variationId?: string;
hasSalePrice?: boolean;
}

export type UseProductInfoProps = {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export function removeNullValuesFromObject(obj: Object) {

return Object.fromEntries(filteredListOfEntries);
}

export function isValidSalePrice(salePrice: number, usualPrice: number) {
return salePrice && usualPrice && salePrice > 0 && salePrice < usualPrice;
}