diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx index 445d5efc2ff..a7ff0e7b741 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx @@ -3,7 +3,13 @@ import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/ import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; -import type {Meta} from '@storybook/web-components'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import type {Decorator} from '@storybook/web-components'; +import {html} from 'lit-html'; + +const styledDivDecorator: Decorator = (story) => { + return html`
${story()}
`; +}; const { decorator: commerceInterfaceDecorator, @@ -14,7 +20,7 @@ const { engineConfig: { context: { view: { - url: 'https://sports.barca.group/browse/promotions/ui-kit-testing', + url: 'https://ui-kit.coveo/atomic/storybook/atomic-product-image', }, language: 'en', country: 'US', @@ -34,6 +40,7 @@ const meta: Meta = { productTemplateDecorator, commerceProductListDecorator, commerceInterfaceDecorator, + styledDivDecorator, ], parameters, play: initializeCommerceInterface, @@ -41,55 +48,22 @@ const meta: Meta = { export default meta; -// export const Default: Story = { -// name: 'atomic-product-image', -// args: { -// 'attributes-fallback': '2', -// play: async (context) => { -// await play(context); -// await playExecuteFirstSearch(context); -// }, -// }, -// }; - -// const {play: playWithMultipleImages} = wrapInCommerceInterface({ -// engineConfig: { -// ...engineConfig, -// preprocessRequest: (r) => { -// const parsed = JSON.parse(r.body as string); -// parsed.query = 'https://sports.barca.group/pdp/SP00003_00001'; -// r.body = JSON.stringify(parsed); -// return r; -// }, -// }, -// }); - -// export const WithMultipleImages: Story = { -// name: 'With multiple images', -// play: async (context) => { -// await playWithMultipleImages(context); -// }, -// }; +export const Default: Story = { + name: 'atomic-product-image', +}; -// export const WithNoImage: Story = { -// name: 'With no image', -// args: { -// 'attributes-field': 'ec_invalid_image_field', -// }, -// play: async (context) => { -// await play(context); -// await playExecuteFirstSearch(context); -// }, -// }; +export const withAFallbackImage: Story = { + name: 'With a fallback image', + args: { + 'attributes-field': 'invalid', + 'attributes-fallback': 'https://sports.barca.group/logos/barca.svg', + }, +}; -// export const WitCustomFallbackImage: Story = { -// name: 'With custom fallback', -// args: { -// 'attributes-field': 'ec_invalid_image_field', -// 'attributes-fallback': 'https://sports.barca.group/logos/barca.svg', -// }, -// play: async (context) => { -// await play(context); -// await playExecuteFirstSearch(context); -// }, -// }; +export const withAnAltTextField: Story = { + tags: ['test'], + name: 'With an alt text field', + args: { + 'attributes-image-alt-field': 'custom_alt_field', + }, +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx index 2d2b2c9e8de..04b1cf66bc6 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.tsx @@ -171,6 +171,10 @@ export class AtomicProductImage implements InitializableComponent { this.product, this.imageAltField ); + // KIT-3620 + // if (isNullOrUndefined(value)) { + // return null; + // } if (Array.isArray(value)) { return value.map((v) => `${v}`.trim()); @@ -209,9 +213,9 @@ export class AtomicProductImage implements InitializableComponent { }); if (this.images.length === 0) { this.validateUrl(this.fallback); - return ( {this.bindings.i18n.t('image-not-found-alt')} { -// test.describe('when clicking on the next button', async ({productImage}) => { -// test.fixme('should navigate to the next image', () => {}); -// test.fixme('should not open the product', () => {}); -// }); -// test.describe('when clicking on the previous button', async ({productImage}) => { -// test.fixme('should navigate to the previous image', () => {}); -// test.fixme('should not open the product', () => {}); -// }); -// }); +test.describe('default', async () => { + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.noCarouselImage.waitFor(); + }); -// default + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); -// accessible + test('should render the image', async ({productImage}) => { + expect(productImage.noCarouselImage).toBeVisible(); + }); -// image alt field + test('should have a default alt text', async ({productImage}) => { + const altText = await productImage.noCarouselImage.getAttribute('alt'); + expect(altText).toEqual('Image 1 out of 1 for Nublu Water Bottle'); + }); -// fallback + test('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); +}); + +test.describe('with a fallback image', async () => { + const FALLBACK = 'https://sports.barca.group/logos/barca.svg'; + + test.describe('when the product image is missing', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails([]); + await productImage.load({story: 'with-a-fallback-image'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + //KIT-3619 + test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); -// as carousel (cant make a story for this) + test.describe('when the product image is invalid', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails(['invalid-image']); + await productImage.load({story: 'with-a-fallback-image'}); + }); -test('as carousel', async ({productImage, page}) => { - await productImage.withMoreImages(); - await productImage.load(); - await page.waitForTimeout(10000); + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + //KIT-3619 + test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); + + test.describe('when the product image is not a string', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomThumbnails([1]); + await productImage.load({story: 'with-a-fallback-image'}); + }); + + test('should render the fallback image', async ({productImage}) => { + const src = await productImage.noCarouselImage.getAttribute('src'); + expect(src).toContain(FALLBACK); + }); + + //KIT-3619 + test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + const aspectRatio = + await productImage.noCarouselImage.getAttribute('class'); + expect(aspectRatio).toEqual('aspect-square'); + }); + }); }); -test('when the image url is not valid, should render the component with fallback image & output error message', () => {}); -test('when the image url is not a string, should render the component with fallback image & output error message', () => {}); +test.describe('with an alt text field', async () => { + test.describe('when imageAltField is a valid string', () => { + const NO_CAROUSEL_CUSTOM_FIELD = 'Nublu Water Bottle'; + const CAROUSEL_CUSTOM_FIELD = 'Blue Lagoon Mat'; + + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField( + 'Nublu Water Bottle', + 'Blue Lagoon Mat' + ); + await productImage.load({story: 'with-an-alt-text-field'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should use the same alt text for all images', async ({ + productImage, + }) => { + const altNoCarousel = + await productImage.noCarouselImage.getAttribute('alt'); + expect(altNoCarousel).toEqual(NO_CAROUSEL_CUSTOM_FIELD); + + const altCarousel = await productImage.carouselImage.getAttribute('alt'); + expect(altCarousel).toEqual(CAROUSEL_CUSTOM_FIELD); + }); + }); + + test.describe('when imageAltField is an array of valid strings', () => { + const NO_CAROUSEL_CUSTOM_FIELDS = [ + 'FIRST Nublu Water Bottle', + 'SECOND Nublu Water Bottle 2', + ]; + const CAROUSEL_CUSTOM_FIELDS = [ + 'FIRST Blue Lagoon Mat', + 'SECOND Blue Lagoon Mat', + ]; + + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField( + NO_CAROUSEL_CUSTOM_FIELDS, + CAROUSEL_CUSTOM_FIELDS + ); + await productImage.load({story: 'with-an-alt-text-field'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should correctly assign alt text for the first image', async ({ + productImage, + }) => { + const noCarouselAlt = + await productImage.noCarouselImage.getAttribute('alt'); + expect(noCarouselAlt).toContain(NO_CAROUSEL_CUSTOM_FIELDS[0]); + + const carouselAlt = await productImage.carouselImage.getAttribute('alt'); + expect(carouselAlt).toContain(CAROUSEL_CUSTOM_FIELDS[0]); + }); + + test('should correctly assign alt text for the last image', async ({ + productImage, + }) => { + await productImage.nextButton.click(); + + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain(CAROUSEL_CUSTOM_FIELDS[1]); + }); + }); + + test.describe('when imageAltField is not specified', () => { + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should generate default alt text for all images', async ({ + productImage, + }) => { + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + }); + }); + + test.describe('when imageAltField is invalid', () => { + test.beforeEach(async ({productImage}) => { + await productImage.load({story: 'with-an-alt-text-field'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + //KIT-3612 + test.fixme( + 'should use the default alt text for all images', + async ({productImage}) => { + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + } + ); + }); + + test.describe('when imageAltField is an empty array', () => { + test.beforeEach(async ({productImage}) => { + await productImage.withCustomField([], []); + await productImage.load({story: 'with-an-alt-text-field'}); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should use the default alt text for all images', async ({ + productImage, + }) => { + expect(await productImage.noCarouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 1 for Nublu Water Bottle' + ); + expect(await productImage.carouselImage.getAttribute('alt')).toEqual( + 'Image 1 out of 2 for Blue Lagoon Mat' + ); + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('alt'); + }) + .toContain('Image 2 out of 2 for Blue Lagoon Mat'); + }); + }); +}); + +test.describe('as a carousel', async () => { + const URL = + 'http://localhost:4400/iframe.html?id=atomic-product-image--default&viewMode=story#sortCriteria=relevance'; + const FIRST_IMAGE = + 'https://images.barca.group/Sports/mj/Trampolines%20%26%20Floats/Huge%20inflatable%20mats/3_Blue/df1a99488425_bottom_right.webp'; + const SECOND_IMAGE = + 'https://images.barca.group/Sports/mj/Trampolines%20%26%20Floats/Huge%20inflatable%20mats/3_Blue/df1a99488425_bottom_left.webp'; + + test.beforeEach(async ({productImage}) => { + await productImage.load(); + await productImage.carouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should render the first image by default', async ({productImage}) => { + await expect(productImage.carouselImage).toBeVisible(); + const src = await productImage.carouselImage.getAttribute('src'); + expect(src).toContain(FIRST_IMAGE); + }); + + test.describe('when clicking the next button', () => { + test.beforeEach(async ({productImage}) => { + await productImage.nextButton.click(); + }); + + test('should navigate to the next image', async ({productImage}) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + + test('should not open the product', async ({page}) => { + expect(page.url()).toEqual(URL); + }); + }); + + test.describe('when clicking the previous button', () => { + test.beforeEach(async ({productImage}) => { + await productImage.previousButton.click(); + }); + + test('should navigate to the previous image', async ({productImage}) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); -test('when rendered as a single image', async ({productImage, page}) => { - await productImage.withNoImage(); - await productImage.load(); - await page.waitForTimeout(10000); + test('should not open the product', async ({page}) => { + expect(page.url()).toEqual(URL); + }); + }); }); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts index ff7d1f91b90..7d1d031d2e3 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/fixture.ts @@ -1,6 +1,4 @@ -import {SortBy} from '@coveo/headless/commerce'; -import {CommerceSuccessResponse} from '@coveo/headless/dist/definitions/api/commerce/common/response'; -import {test as base, Page} from '@playwright/test'; +import {test as base} from '@playwright/test'; import { AxeFixture, makeAxeBuilder, @@ -17,53 +15,5 @@ export const test = base.extend({ await use(new ProductImageObject(page)); }, }); -export {expect} from '@playwright/test'; -export async function setImages(page: Page, urls: string[]) { - await page.route('**/v2/search', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - facets: [], - pagination: { - page: 0, - perPage: 1, - totalEntries: 1, - totalPages: 1, - }, - products: [ - { - additionalFields: {}, - children: [], - clickUri: '', - ec_brand: '', - ec_category: [], - ec_color: '', - ec_description: '', - ec_gender: '', - ec_images: urls, - ec_in_stock: true, - ec_item_group_id: '', - ec_listing: '', - ec_name: 'name', - ec_price: 0, - ec_product_id: '', - ec_promo_price: 0, - ec_rating: 0, - ec_shortdesc: '', - ec_thumbnails: urls, - permanentid: 'permanentid', - totalNumberOfChildren: 0, - }, - ], - responseId: '', - sort: { - appliedSort: {sortCriteria: SortBy.Relevance}, - availableSorts: [], - }, - triggers: [], - } as CommerceSuccessResponse), - }); - }); -} +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts index fcba569999a..f7bae26e7f8 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/page-object.ts @@ -6,15 +6,28 @@ export class ProductImageObject extends BasePageObject<'atomic-product-image'> { super(page, 'atomic-product-image'); } - async withMoreImages() { + get noCarouselImage() { + return this.page.getByRole('img').nth(0); + } + + get carouselImage() { + return this.page.getByRole('img').nth(1); + } + + get nextButton() { + return this.page.getByRole('button', {name: 'Next'}); + } + + get previousButton() { + return this.page.getByRole('button', {name: 'Previous'}); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async withCustomThumbnails(thumbnails: any[]) { await this.page.route('**/commerce/v2/listing', async (route) => { const response = await route.fetch(); const body = await response.json(); - body.products[0].ec_thumbnails = [ - 'https://images.barca.group/Sports/mj/Sandals%20%26%20Shoes/Sandals/47_Blue_Women_Logo_Flip_Flop/7940686db76f_bottom_left.webp', - 'https://images.barca.group/Sports/mj/Clothing/T-Shirts/29_Women_Blue_Cotton/2b1a880a2e30_bottom_left.webp', - 'https://images.barca.group/Sports/mj/Clothing/T-Shirts/29_Women_Blue_Elastane/892ee4fe4145_bottom_left.webp', - ]; + body.products[0].ec_thumbnails = thumbnails; await route.fulfill({ response, @@ -24,11 +37,15 @@ export class ProductImageObject extends BasePageObject<'atomic-product-image'> { return this; } - async withNoImage() { + async withCustomField( + fieldNoCarousel: string | string[], + fieldCarousel: string | string[] + ) { await this.page.route('**/commerce/v2/listing', async (route) => { const response = await route.fetch(); const body = await response.json(); - body.products[0].ec_thumbnails = []; + body.products[0].custom_alt_field = fieldNoCarousel; + body.products[1].custom_alt_field = fieldCarousel; await route.fulfill({ response,