From bc09766b106026a1c7b29c76167100d9dd88f23e Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:25:14 -0400 Subject: [PATCH 01/11] test(atomic): add tests & stories for atomic-product-image (#4469) https://coveord.atlassian.net/browse/KIT-3268 --------- Co-authored-by: Frederic Beaudoin --- packages/atomic/src/components.d.ts | 2 + .../atomic-product-image.new.stories.tsx | 70 +++ .../atomic-product-image.tsx | 10 +- .../e2e/atomic-product-image.e2e.ts | 411 +++++++++++++++++- .../atomic-product-image/e2e/fixture.ts | 1 + .../atomic-product-image/e2e/page-object.ts | 54 ++- 6 files changed, 533 insertions(+), 15 deletions(-) create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index 1ed53e7e6ed..1daaa1c34ba 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -2051,6 +2051,7 @@ export namespace Components { "field": string; /** * The product field that contains the alt text for the images. This will look for the field in the product object first, then in the product.additionalFields object. The field can be a string or an array of strings. If the value of the field is a string, it will be used as the alt text for all the images. If the value of the field is an array of strings, the alt text will be used in the order of the images. If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ "imageAltField"?: string; /** @@ -7935,6 +7936,7 @@ declare namespace LocalJSX { "field"?: string; /** * The product field that contains the alt text for the images. This will look for the field in the product object first, then in the product.additionalFields object. The field can be a string or an array of strings. If the value of the field is a string, it will be used as the alt text for all the images. If the value of the field is an array of strings, the alt text will be used in the order of the images. If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ "imageAltField"?: string; } 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 new file mode 100644 index 00000000000..260294c6f75 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/atomic-product-image.new.stories.tsx @@ -0,0 +1,70 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +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, 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, + play: initializeCommerceInterface, +} = wrapInCommerceInterface({ + skipFirstSearch: false, + type: 'product-listing', + engineConfig: { + context: { + view: { + url: 'https://ui-kit.coveo/atomic/storybook/atomic-product-image', + }, + language: 'en', + country: 'US', + currency: 'USD', + }, + }, +}); +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: productTemplateDecorator} = wrapInProductTemplate(); + +const meta: Meta = { + component: 'atomic-product-image', + title: 'Atomic-Commerce/Product Template Components/ProductImage', + id: 'atomic-product-image', + render: renderComponent, + decorators: [ + productTemplateDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + styledDivDecorator, + ], + parameters, + play: initializeCommerceInterface, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-image', +}; + +export const withAFallbackImage: Story = { + name: 'With a fallback image', + args: { + 'attributes-field': 'invalid', + 'attributes-fallback': 'https://sports.barca.group/logos/barca.svg', + }, +}; + +export const withAnAltTextField: Story = { + name: 'With an alt text field', + args: { + 'attributes-field': 'invalid', + 'attributes-fallback': 'invalid', + 'attributes-image-alt-field': 'ec_name', + }, +}; 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 94be915b12f..8f3ecaf609a 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 @@ -47,6 +47,7 @@ export class AtomicProductImage implements InitializableComponent { * If the value of the field is an array of strings, the alt text will be used in the order of the images. * * If the field is not specified, or does not contain a valid value, the alt text will be set to "Image {index} out of {totalImages} for {productName}". + * @type {string} */ @Prop({reflect: true}) imageAltField?: string; @@ -170,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()); @@ -208,9 +213,10 @@ export class AtomicProductImage implements InitializableComponent { }); if (this.images.length === 0) { this.validateUrl(this.fallback); - return ( {this.bindings.i18n.t('image-not-found-alt')} { } return ( - // TODO: handle small/icon image sizes better on mobile + // TODO - KIT-3612 : handle small/icon image sizes better on mobile { -// 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', () => {}); -// }); -// }); +/* eslint-disable @cspell/spellchecker */ +import {test, expect} from './fixture'; + +test.describe('default', async () => { + 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 render the image', async ({productImage}) => { + expect(productImage.noCarouselImage).toBeVisible(); + }); + + 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'); + }); + + 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 custom 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'); + }); + }); + + 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('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.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', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_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', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_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', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_field', + }, + }); + await productImage.noCarouselImage.waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + //TODO: KIT-3620 + test.fixme( + 'should use the default alt text for all images', + async ({productImage, page}) => { + await page.waitForTimeout(10000); + 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', + args: { + field: 'ec_thumbnails', + fallback: undefined, + imageAltField: 'custom_alt_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 navigate to the first image if the last image is reached', async ({ + productImage, + }) => { + await productImage.nextButton.click(); + await expect + .poll(async () => { + return await productImage.carouselImage.getAttribute('src'); + }) + .toContain(FIRST_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 last image if the first image is reached', async ({ + productImage, + }) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + + test('should navigate to the previous image', async ({productImage}) => { + await productImage.previousButton.click(); + + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(FIRST_IMAGE); + }); + + test('should not open the product', async ({page}) => { + expect(page.url()).toEqual(URL); + }); + }); + + test.describe('when clicking the indicator dot', () => { + test('should navigate to the corresponding image', async ({ + productImage, + }) => { + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(FIRST_IMAGE); + + await productImage.indicatorDot.click(); + + await expect + .poll(async () => { + const src = await productImage.carouselImage.getAttribute('src'); + return src; + }) + .toContain(SECOND_IMAGE); + }); + }); +}); 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 ca73e2a7976..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 @@ -15,4 +15,5 @@ export const test = base.extend({ await use(new ProductImageObject(page)); }, }); + 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 30a89aebb3a..063c75d6c6b 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 @@ -5,5 +5,57 @@ export class ProductImageObject extends BasePageObject<'atomic-product-image'> { constructor(page: Page) { super(page, 'atomic-product-image'); } - // TODO tests + + 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'}); + } + + get indicatorDot() { + return this.page.getByRole('listitem').nth(1); + } + + // 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 = thumbnails; + + await route.fulfill({ + response, + json: body, + }); + }); + return this; + } + + 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].custom_alt_field = fieldNoCarousel; + body.products[1].custom_alt_field = fieldCarousel; + + await route.fulfill({ + response, + json: body, + }); + }); + return this; + } } From 1fb3864e73778721a5ad69a8bc9e6ec91490bc27 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:50:33 -0400 Subject: [PATCH 02/11] fix(atomic): atomic-product-image crashes when giving an invalid image-alt-field value (#4482) https://coveord.atlassian.net/browse/KIT-3620 --------- Co-authored-by: Frederic Beaudoin --- .../atomic-product-image.tsx | 9 +++-- .../e2e/atomic-product-image.e2e.ts | 37 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) 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 8f3ecaf609a..240db8fb033 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 @@ -1,3 +1,4 @@ +import {isNullOrUndefined} from '@coveo/bueno'; import {Product, ProductTemplatesHelpers} from '@coveo/headless/commerce'; import {Component, h, Prop, Element, State, Method} from '@stencil/core'; import { @@ -171,10 +172,10 @@ export class AtomicProductImage implements InitializableComponent { this.product, this.imageAltField ); - // KIT-3620 - // if (isNullOrUndefined(value)) { - // return null; - // } + + if (isNullOrUndefined(value)) { + return null; + } if (Array.isArray(value)) { return value.map((v) => `${v}`.trim()); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/atomic-product-image.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/atomic-product-image.e2e.ts index 59729122121..1b061b47f11 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/atomic-product-image.e2e.ts +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-image/e2e/atomic-product-image.e2e.ts @@ -230,25 +230,24 @@ test.describe('with an alt text field', async () => { expect(accessibilityResults.violations.length).toEqual(0); }); - //TODO: KIT-3620 - test.fixme( - 'should use the default alt text for all images', - async ({productImage, page}) => { - await page.waitForTimeout(10000); - 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('should use the default alt text for all images', async ({ + productImage, + page, + }) => { + await page.waitForTimeout(10000); + 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', () => { From 816f529f5ce8168e64dc766800321a9a1371dad0 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:13:15 -0400 Subject: [PATCH 03/11] fix(atomic): atomic-product-image fallback image is not using 1:1 ratio (#4483) https://coveord.atlassian.net/browse/KIT-3619 --------- Co-authored-by: Frederic Beaudoin --- .../atomic-product-image/atomic-product-image.tsx | 2 +- .../atomic-product-image/e2e/atomic-product-image.e2e.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) 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 240db8fb033..f099ac61e61 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 @@ -216,8 +216,8 @@ export class AtomicProductImage implements InitializableComponent { this.validateUrl(this.fallback); return ( {this.bindings.i18n.t('image-not-found-alt')} { expect(src).toContain(FALLBACK); }); - //KIT-3619 - test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + test('should have a 1:1 aspect ratio', async ({productImage}) => { const aspectRatio = await productImage.noCarouselImage.getAttribute('class'); expect(aspectRatio).toEqual('aspect-square'); @@ -62,8 +61,7 @@ test.describe('with a custom fallback image', async () => { expect(src).toContain(FALLBACK); }); - //KIT-3619 - test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + test('should have a 1:1 aspect ratio', async ({productImage}) => { const aspectRatio = await productImage.noCarouselImage.getAttribute('class'); expect(aspectRatio).toEqual('aspect-square'); @@ -81,8 +79,7 @@ test.describe('with a custom fallback image', async () => { expect(src).toContain(FALLBACK); }); - //KIT-3619 - test.fixme('should have a 1:1 aspect ratio', async ({productImage}) => { + test('should have a 1:1 aspect ratio', async ({productImage}) => { const aspectRatio = await productImage.noCarouselImage.getAttribute('class'); expect(aspectRatio).toEqual('aspect-square'); From 35ab2823683ff84d3ef90ab4f56439ee4a48b2de Mon Sep 17 00:00:00 2001 From: Felix Perron-Brault Date: Tue, 8 Oct 2024 16:46:04 -0400 Subject: [PATCH 04/11] fix(atomic): prevent quickview from reopening when changing tab (#4508) This PR prevents the quickview from reopening when changing tab. This issue occurs when the two tabs have different result lists enabled. https://coveord.atlassian.net/browse/KIT-3557 --- .../atomic-quickview/atomic-quickview.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx b/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx index cb91b5aa9f6..145ca3fd6ea 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx +++ b/packages/atomic/src/components/search/result-template-components/atomic-quickview/atomic-quickview.tsx @@ -139,9 +139,12 @@ export class AtomicQuickview implements InitializableComponent { this.quickview.fetchResultContent(); } - public render() { + componentWillUpdate(): void { this.addQuickviewModalIfNeeded(); this.updateModalContent(); + } + + public render() { if (this.quickviewState.resultHasPreview) { return (