Skip to content

Commit

Permalink
Merge pull request #538 from shopgate/PWA-1628-product-card-render-props
Browse files Browse the repository at this point in the history
Added missing props to the ProductCard from the themeApi
  • Loading branch information
devbucket committed Feb 14, 2019
2 parents 87b470e + d793163 commit 3a0a214
Show file tree
Hide file tree
Showing 16 changed files with 10,854 additions and 34 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

51 changes: 42 additions & 9 deletions themes/theme-gmd/themeApi/ProductCard/components/Render/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@ import style from './style';
* @param {Object} props The component props.
* @param {Object} props.product The product data.
* @param {string} props.url The product route url.
* @param {boolean} props.hidePrice Whether the price should be hidden.
* @param {boolean} props.hideRating Whether the rating should be hidden.
* @param {boolean} props.hideName Whether the name should be hidden.
* @param {number} props.titleRows The max number of rows for the product title.
* @returns {JSX}
*/
function ProductCardRender({ product, url }) {
function ProductCardRender({
product,
url,
hideName,
hidePrice,
hideRating,
titleRows,
}) {
const {
featuredImageUrl,
id,
Expand All @@ -26,21 +37,43 @@ function ProductCardRender({ product, url }) {
return (
<Link tagName="a" href={url}>
<ProductImage itemProp="image" src={featuredImageUrl} alt={name} />
{(price.discount > 0) && <Badge productId={id} value={-price.discount} />}
<div className={style}>
{(rating && rating.average > 0) && (
<RatingStars value={product.rating.average} />
)}
<Title title={product.name} />
<Price price={product.price} productId={id} />
</div>
{(!hidePrice && price.discount > 0) && <Badge productId={id} value={-price.discount} />}

{(!(hidePrice && hideRating)) && (
<div className={style}>
{(!hideRating && rating && rating.average > 0) && (
<RatingStars value={product.rating.average} />
)}
{!hideName && (
<Title title={product.name} rows={titleRows} />
)}
{!hidePrice && (
<Price price={product.price} productId={id} />
)}
</div>
)}
</Link>
);
}

ProductCardRender.Badge = Badge;
ProductCardRender.Price = Price;
ProductCardRender.Title = Title;

ProductCardRender.propTypes = {
product: PropTypes.shape().isRequired,
url: PropTypes.string.isRequired,
hideName: PropTypes.bool,
hidePrice: PropTypes.bool,
hideRating: PropTypes.bool,
titleRows: PropTypes.number,
};

ProductCardRender.defaultProps = {
hideName: false,
hidePrice: false,
hideRating: false,
titleRows: 3,
};

export default ProductCardRender;
105 changes: 105 additions & 0 deletions themes/theme-gmd/themeApi/ProductCard/components/Render/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import mockRenderOptions from '@shopgate/pwa-common/helpers/mocks/mockRenderOptions';
import { mockProductId, mockProduct } from '../../mock';
import ProductCardRender from './index';

const defaultProps = {
url: '/some/url',
productId: mockProductId,
product: mockProduct,
};

/**
* @param {Object} additionalProps Additional component props
* @param {Object} state Redux state.
* @returns {JSX}
*/
const renderComponent = (additionalProps = {}) => {
const props = {
...defaultProps,
...additionalProps,
};

const store = configureStore()();
return mount(
<Provider store={store}>
<ProductCardRender {...props} />
</Provider>,
mockRenderOptions
);
};

describe('<ProductCardRender />', () => {
it('should render as expected', () => {
const wrapper = renderComponent();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Portal').length).toBe(6);
expect(wrapper.find('Link').prop('href')).toEqual(defaultProps.url);
expect(wrapper.find('ProductCardBadge').exists()).toBe(true);
expect(wrapper.find('ProductCardBadge').text()).toEqual(`-${mockProduct.price.discount}%`);
expect(wrapper.find('RatingStars').exists()).toBe(true);
expect(wrapper.find('RatingStars').prop('value')).toBe(mockProduct.rating.average);
expect(wrapper.find('ProductCardTitle').exists()).toBe(true);
expect(wrapper.find('ProductCardTitle').text()).toBe(mockProduct.name);
expect(wrapper.find('ProductCardTitle').prop('rows')).toBe(ProductCardRender.defaultProps.titleRows);
const productCardPrice = wrapper.find('ProductCardPrice');
expect(productCardPrice.exists()).toBe(true);
expect(productCardPrice.find('ProductGridPrice').prop('price')).toEqual(mockProduct.price);
});

it('should render without name', () => {
const wrapper = renderComponent({ hideName: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ProductCardTitle').exists()).toBe(false);
});

it('should render without rating', () => {
const wrapper = renderComponent({ hideRating: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('RatingStars').exists()).toBe(false);
});

it('should render without prices', () => {
const wrapper = renderComponent({ hidePrice: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ProductCardBadge').exists()).toBe(false);
expect(wrapper.find('ProductCardPrice').exists()).toBe(false);
});

it('should render with one row for the product name', () => {
const wrapper = renderComponent({ titleRows: 1 });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ProductCardTitle').find('Ellipsis').prop('rows')).toBe(1);
});

it('should not render the discount badge when the is no discount', () => {
const wrapper = renderComponent({
product: {
...mockProduct,
price: {
...mockProduct.price,
discount: 0,
},
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ProductCardBadge').exists()).toBe(false);
});

it('should not render the rating stars when there is no average rating', () => {
const wrapper = renderComponent({
product: {
...mockProduct,
rating: {
...mockProduct.rating,
average: 0,
},
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('RatingStars').exists()).toBe(false);
});
});
11 changes: 4 additions & 7 deletions themes/theme-gmd/themeApi/ProductCard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { bin2hex } from '@shopgate/pwa-common/helpers/data';
import { ITEM_PATH } from '@shopgate/pwa-common-commerce/product/constants';
import Badge from './components/Badge';
import Title from './components/Title';
import Price from './components/Price';
import Render from './components/Render';
import connect from './connector';
import styles from './style';
Expand Down Expand Up @@ -43,9 +40,7 @@ function ProductCard({
);
}

ProductCard.Badge = Badge;
ProductCard.Price = Price;
ProductCard.Title = Title;
ProductCard.Content = Render;

ProductCard.propTypes = {
product: PropTypes.shape(),
Expand All @@ -56,9 +51,11 @@ ProductCard.propTypes = {

ProductCard.defaultProps = {
product: null,
render: Render,
render: props => <Render {...props} />,
shadow: true,
style: {},
};

export default connect(ProductCard);

export { ProductCard as ProductCardUnwrapped };
72 changes: 72 additions & 0 deletions themes/theme-gmd/themeApi/ProductCard/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { bin2hex } from '@shopgate/pwa-common/helpers/data';
import { ITEM_PATH } from '@shopgate/pwa-common-commerce/product/constants';
import mockRenderOptions from '@shopgate/pwa-common/helpers/mocks/mockRenderOptions';
import { mockProductId, mockProduct } from './mock';
import ProductCard, { ProductCardUnwrapped } from './index';

/**
* Creates a state for a mocked store.
* @param {Object} product A product.
* @param {string} productId The id for the product.
* @returns {Object}
*/
export const createMockState = (product = mockProduct) => ({
product: {
productsById: {
[product.id]: {
productData: product,
},
},
},
});

/**
* @param {Object} props Component props.
* @param {Object} state Redux state.
* @returns {JSX}
*/
const renderComponent = (props = {}, state = createMockState()) => {
const store = configureStore()(state);
return mount(
<Provider store={store}>
<ProductCard {...props} />
</Provider>,
mockRenderOptions
);
};

describe('<ProductCard />', () => {
it('should not render when no product could be found', () => {
const wrapper = renderComponent();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(ProductCardUnwrapped).isEmptyRender()).toBe(true);
});

it('should render as expected', () => {
const wrapper = renderComponent({ productId: mockProductId });
expect(wrapper).toMatchSnapshot();

const renderWrapper = wrapper.find('ProductCardRender');
expect(renderWrapper.prop('url')).toBe(`${ITEM_PATH}/${bin2hex(mockProductId)}`);
expect(renderWrapper.prop('product')).toBe(mockProduct);
});

it('should render with a custom render prop', () => {
const text = 'Custom Output';

/**
* @returns {JSX}
*/
const render = () => (
<div>{text}</div>
);

const wrapper = renderComponent({ productId: mockProductId, render });
expect(wrapper).toMatchSnapshot();
expect(wrapper.text()).toBe(text);
});
});
18 changes: 18 additions & 0 deletions themes/theme-gmd/themeApi/ProductCard/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const mockProductId = 'PR0DUCTID';
export const mockProduct = {
id: mockProductId,
name: 'Product Name',
featuredImageUrl: null,
price: {
currency: 'EUR',
unitPrice: 5,
unitPriceStriked: 10,
discount: 50,
info: 'Price Info',
},
rating: {
count: 4,
average: 80,
reviewCount: 3,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function LiveshoppingItem({ productId }) {
{price.discount > 0 &&
<Discount discount={price.discount} productId={productId} />
}
<ProductCard.Title title={name} style={styles.title} />
<ProductCard.Content.Title title={name} style={styles.title} />
{timeout &&
<CountdownTimer className={styles.timer} timeout={timeout / 1000} />
}
Expand Down

0 comments on commit 3a0a214

Please sign in to comment.