* Fix image link in all products block.

- add alt text on product image
- prevent the appearance of both product and placeholder image at the same time
- make anchor with placeholder image accessible

* Add testing for product-elements/image block.
This commit is contained in:
Raluca Stan 2021-01-28 14:23:20 +01:00 committed by GitHub
parent f2012158f3
commit e7af435f08
2 changed files with 179 additions and 50 deletions

View File

@ -2,7 +2,8 @@
* External dependencies
*/
import PropTypes from 'prop-types';
import { useState } from '@wordpress/element';
import { useState, Fragment } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
import {
@ -10,7 +11,6 @@ import {
useProductDataContext,
} from '@woocommerce/shared-context';
import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isEmpty } from 'lodash';
/**
* Internal dependencies
@ -29,10 +29,10 @@ import './style.scss';
* @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
* @return {*} The component.
*/
const Block = ( {
export const Block = ( {
className,
imageSizing = 'full-size',
productLink = true,
productLink: showProductLink = true,
showSaleBadge,
saleBadgeAlign = 'right',
} ) => {
@ -56,8 +56,19 @@ const Block = ( {
</div>
);
}
const image = ! isEmpty( product.images ) ? product.images[ 0 ] : null;
const hasProductImages = !! product.images.length;
const image = hasProductImages ? product.images[ 0 ] : null;
const ParentComponent = showProductLink ? 'a' : Fragment;
const anchorLabel = sprintf(
/* Translators: %s is referring to the product name */
__( 'Link to %s', 'woo-gutenberg-products-block' ),
product.name
);
const anchorProps = {
href: product.permalink,
rel: 'nofollow',
...( ! hasProductImages && { 'aria-label': anchorLabel } ),
};
return (
<div
@ -69,37 +80,21 @@ const Block = ( {
}
) }
>
{ productLink ? (
<a href={ product.permalink } rel="nofollow">
{ !! showSaleBadge && (
<ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) }
<Image
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
showFullSize={ imageSizing !== 'cropped' }
<ParentComponent { ...( showProductLink && anchorProps ) }>
{ !! showSaleBadge && (
<ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
</a>
) : (
<>
{ !! showSaleBadge && (
<ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) }
<Image
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
showFullSize={ imageSizing !== 'cropped' }
/>
</>
) }
) }
<Image
fallbackAlt={ product.name }
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
showFullSize={ imageSizing !== 'cropped' }
/>
</ParentComponent>
</div>
);
};
@ -110,28 +105,22 @@ const ImagePlaceholder = () => {
);
};
const Image = ( { image, onLoad, loaded, showFullSize } ) => {
const Image = ( { image, onLoad, loaded, showFullSize, fallbackAlt } ) => {
const { thumbnail, src, srcset, sizes, alt } = image || {};
let imageProps = {
alt,
const imageProps = {
alt: alt || fallbackAlt,
onLoad,
hidden: ! loaded,
src: thumbnail,
...( showFullSize && { src, srcSet: srcset, sizes } ),
};
if ( showFullSize ) {
imageProps = {
...imageProps,
src,
srcSet: srcset,
sizes,
};
}
return (
<>
{ /* eslint-disable-next-line jsx-a11y/alt-text */ }
<img { ...imageProps } />
{ imageProps.src && (
/* eslint-disable-next-line jsx-a11y/alt-text */
<img data-testid="product-image" { ...imageProps } />
) }
{ ! loaded && <ImagePlaceholder /> }
</>
);
@ -139,6 +128,7 @@ const Image = ( { image, onLoad, loaded, showFullSize } ) => {
Block.propTypes = {
className: PropTypes.string,
fallbackAlt: PropTypes.string,
productLink: PropTypes.bool,
showSaleBadge: PropTypes.bool,
saleBadgeAlign: PropTypes.string,

View File

@ -0,0 +1,139 @@
/**
* External dependencies
*/
import { render, fireEvent } from '@testing-library/react';
import { ProductDataContextProvider } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import { Block } from '../block';
jest.mock( '@woocommerce/block-settings', () => ( {
__esModule: true,
PLACEHOLDER_IMG_SRC: 'placeholder.jpg',
} ) );
const productWithoutImages = {
name: 'Test product',
id: 1,
fallbackAlt: 'Test product',
permalink: 'http://test.com/product/test-product/',
images: [],
};
const productWithImages = {
name: 'Test product',
id: 1,
fallbackAlt: 'Test product',
permalink: 'http://test.com/product/test-product/',
images: [
{
id: 56,
src: 'logo-1.jpg',
thumbnail: 'logo-1-324x324.jpg',
srcset:
'logo-1.jpg 800w, logo-1-300x300.jpg 300w, logo-1-150x150.jpg 150w, logo-1-768x767.jpg 768w, logo-1-324x324.jpg 324w, logo-1-416x415.jpg 416w, logo-1-100x100.jpg 100w',
sizes: '(max-width: 800px) 100vw, 800px',
name: 'logo-1.jpg',
alt: '',
},
{
id: 55,
src: 'beanie-with-logo-1.jpg',
thumbnail: 'beanie-with-logo-1-324x324.jpg',
srcset:
'beanie-with-logo-1.jpg 800w, beanie-with-logo-1-300x300.jpg 300w, beanie-with-logo-1-150x150.jpg 150w, beanie-with-logo-1-768x768.jpg 768w, beanie-with-logo-1-324x324.jpg 324w, beanie-with-logo-1-416x416.jpg 416w, beanie-with-logo-1-100x100.jpg 100w',
sizes: '(max-width: 800px) 100vw, 800px',
name: 'beanie-with-logo-1.jpg',
alt: '',
},
],
};
describe( 'Product Image Block', () => {
describe( 'with product link', () => {
test( 'should render an anchor with the product image', () => {
const component = render(
<ProductDataContextProvider product={ productWithImages }>
<Block productLink />
</ProductDataContextProvider>
);
// use testId as alt is added after image is loaded
const image = component.getByTestId( 'product-image' );
fireEvent.load( image );
const productImage = component.getByAltText(
productWithImages.name
);
expect( productImage.getAttribute( 'src' ) ).toBe(
productWithImages.images[ 0 ].src
);
const anchor = productImage.closest( 'a' );
expect( anchor.getAttribute( 'href' ) ).toBe(
productWithImages.permalink
);
} );
test( 'should render an anchor with the placeholder image', () => {
const component = render(
<ProductDataContextProvider product={ productWithoutImages }>
<Block productLink />
</ProductDataContextProvider>
);
const placeholderImage = component.getByAltText( '' );
expect( placeholderImage.getAttribute( 'src' ) ).toBe(
'placeholder.jpg'
);
const anchor = placeholderImage.closest( 'a' );
expect( anchor.getAttribute( 'href' ) ).toBe(
productWithoutImages.permalink
);
expect( anchor.getAttribute( 'aria-label' ) ).toBe(
`Link to ${ productWithoutImages.name }`
);
} );
} );
describe( 'without product link', () => {
test( 'should render the product image without an anchor wrapper', () => {
const component = render(
<ProductDataContextProvider product={ productWithImages }>
<Block productLink={ false } />
</ProductDataContextProvider>
);
const image = component.getByTestId( 'product-image' );
fireEvent.load( image );
const productImage = component.getByAltText(
productWithImages.name
);
expect( productImage.getAttribute( 'src' ) ).toBe(
productWithImages.images[ 0 ].src
);
const anchor = productImage.closest( 'a' );
expect( anchor ).toBe( null );
} );
test( 'should render the placeholder image without an anchor wrapper', () => {
const component = render(
<ProductDataContextProvider product={ productWithoutImages }>
<Block productLink={ false } />
</ProductDataContextProvider>
);
const placeholderImage = component.getByAltText( '' );
expect( placeholderImage.getAttribute( 'src' ) ).toBe(
'placeholder.jpg'
);
const anchor = placeholderImage.closest( 'a' );
expect( anchor ).toBe( null );
} );
} );
} );