From 949d6ca9fb356f779444fbc52643f3ccb8c28a8a Mon Sep 17 00:00:00 2001 From: Louis Bompart Date: Mon, 4 Nov 2024 16:22:14 -0500 Subject: [PATCH] feat(atomic, commerce): create atomic-product-multi-value-text component (#4224) https://coveord.atlassian.net/browse/KIT-3440 Implement the `atomic-product-multi-value-text` component to render the values of a multi-value string field. * **New Component**: Add `atomic-product-multi-value-text` component in `packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx`. - Combine functionalities from `atomic-product-text`, `atomic-result-text`, and `atomic-result-multi-value-text`. - Use `ProductContext` to get the product data. - Use `getFieldValueCaption` to get the field value caption. - Render the multi-value text field values. * **Storybook**: Add a new story for `atomic-product-multi-value-text` in `packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx`. - Test rendering of multi-value text field values. - Test rendering with different `max-values-to-display` values. - Test rendering with custom slot values. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/coveo/ui-kit?shareId=02336614-673d-4772-9196-21872fd10d36). --- KIT-3440 --------- Co-authored-by: Frederic Beaudoin Co-authored-by: GitHub Actions Bot <> Co-authored-by: ylakhdar --- .../atomic-angular.module.ts | 2 + .../src/lib/stencil-generated/components.ts | 22 ++ .../stencil-generated/commerce/index.ts | 1 + packages/atomic/src/components.d.ts | 49 +++++ ...c-product-multi-value-text.new.stories.tsx | 106 ++++++++++ .../atomic-product-multi-value-text.pcss | 19 ++ .../atomic-product-multi-value-text.tsx | 200 ++++++++++++++++++ .../atomic-product-multi-value-text.e2e.ts | 162 ++++++++++++++ .../e2e/fixture.ts | 19 ++ .../e2e/page-object.ts | 48 +++++ .../atomic-result-multi-value-text.tsx | 8 +- .../pages/examples/commerce-website/cart.html | 1 + .../examples/commerce-website/homepage.html | 1 + .../commerce-website/listing-pants.html | 1 + .../listing-surf-accessories.html | 1 + .../commerce-website/listing-towels.html | 1 + .../examples/commerce-website/search.html | 13 ++ 17 files changed, 647 insertions(+), 7 deletions(-) create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts create mode 100644 packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts index 200e7f34a41..9b316b4cbbf 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts @@ -66,6 +66,7 @@ AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, +AtomicProductMultiValueText, AtomicProductNumericFieldValue, AtomicProductPrice, AtomicProductRating, @@ -210,6 +211,7 @@ AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, +AtomicProductMultiValueText, AtomicProductNumericFieldValue, AtomicProductPrice, AtomicProductRating, diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts index 1fca16c1bac..cf1daa76a9e 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts @@ -1388,6 +1388,28 @@ export class AtomicProductLink { export declare interface AtomicProductLink extends Components.AtomicProductLink {} +@ProxyCmp({ + inputs: ['delimiter', 'field', 'maxValuesToDisplay'] +}) +@Component({ + selector: 'atomic-product-multi-value-text', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['delimiter', 'field', 'maxValuesToDisplay'], +}) +export class AtomicProductMultiValueText { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface AtomicProductMultiValueText extends Components.AtomicProductMultiValueText {} + + @ProxyCmp({ inputs: ['field'] }) diff --git a/packages/atomic-react/src/components/stencil-generated/commerce/index.ts b/packages/atomic-react/src/components/stencil-generated/commerce/index.ts index ed83c86f8f1..9c6d44ac100 100644 --- a/packages/atomic-react/src/components/stencil-generated/commerce/index.ts +++ b/packages/atomic-react/src/components/stencil-generated/commerce/index.ts @@ -45,6 +45,7 @@ export const AtomicProductExcerpt = /*@__PURE__*/createReactComponent('atomic-product-field-condition'); export const AtomicProductImage = /*@__PURE__*/createReactComponent('atomic-product-image'); export const AtomicProductLink = /*@__PURE__*/createReactComponent('atomic-product-link'); +export const AtomicProductMultiValueText = /*@__PURE__*/createReactComponent('atomic-product-multi-value-text'); export const AtomicProductNumericFieldValue = /*@__PURE__*/createReactComponent('atomic-product-numeric-field-value'); export const AtomicProductPrice = /*@__PURE__*/createReactComponent('atomic-product-price'); export const AtomicProductRating = /*@__PURE__*/createReactComponent('atomic-product-rating'); diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index 1ecb7eb68d3..8c3519111af 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -2162,6 +2162,23 @@ export namespace Components { */ "hrefTemplate"?: string; } + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface AtomicProductMultiValueText { + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + "delimiter": string | null; + /** + * The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + "field": string; + /** + * The maximum number of field values to display. If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + "maxValuesToDisplay": number; + } /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -4972,6 +4989,15 @@ declare global { prototype: HTMLAtomicProductLinkElement; new (): HTMLAtomicProductLinkElement; }; + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface HTMLAtomicProductMultiValueTextElement extends Components.AtomicProductMultiValueText, HTMLStencilElement { + } + var HTMLAtomicProductMultiValueTextElement: { + prototype: HTMLAtomicProductMultiValueTextElement; + new (): HTMLAtomicProductMultiValueTextElement; + }; /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -6098,6 +6124,7 @@ declare global { "atomic-product-field-condition": HTMLAtomicProductFieldConditionElement; "atomic-product-image": HTMLAtomicProductImageElement; "atomic-product-link": HTMLAtomicProductLinkElement; + "atomic-product-multi-value-text": HTMLAtomicProductMultiValueTextElement; "atomic-product-numeric-field-value": HTMLAtomicProductNumericFieldValueElement; "atomic-product-price": HTMLAtomicProductPriceElement; "atomic-product-rating": HTMLAtomicProductRatingElement; @@ -8189,6 +8216,23 @@ declare namespace LocalJSX { */ "hrefTemplate"?: string; } + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface AtomicProductMultiValueText { + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + "delimiter"?: string | null; + /** + * The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + "field": string; + /** + * The maximum number of field values to display. If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + "maxValuesToDisplay"?: number; + } /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -9861,6 +9905,7 @@ declare namespace LocalJSX { "atomic-product-field-condition": AtomicProductFieldCondition; "atomic-product-image": AtomicProductImage; "atomic-product-link": AtomicProductLink; + "atomic-product-multi-value-text": AtomicProductMultiValueText; "atomic-product-numeric-field-value": AtomicProductNumericFieldValue; "atomic-product-price": AtomicProductPrice; "atomic-product-rating": AtomicProductRating; @@ -10327,6 +10372,10 @@ declare module "@stencil/core" { * @alpha The `atomic-product-link` component automatically transforms a search product title into a clickable link that points to the original item. */ "atomic-product-link": LocalJSX.AtomicProductLink & JSXBase.HTMLAttributes; + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + "atomic-product-multi-value-text": LocalJSX.AtomicProductMultiValueText & JSXBase.HTMLAttributes; /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx new file mode 100644 index 00000000000..10de7eb7be0 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx @@ -0,0 +1,106 @@ +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 {getSampleCommerceEngineConfiguration} from '@coveo/headless/commerce'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import {html} from 'lit/static-html.js'; + +const baseConfiguration = getSampleCommerceEngineConfiguration(); + +const {decorator: productDecorator} = wrapInProductTemplate(); +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: commerceInterfaceDecorator, play} = wrapInCommerceInterface({ + engineConfig: { + ...baseConfiguration, + context: { + ...baseConfiguration.context, + view: { + url: 'https://sports.barca.group/browse/promotions/ui-kit-testing-product-multi-value-text', + }, + }, + }, + type: 'product-listing', +}); + +const meta: Meta = { + component: 'atomic-product-multi-value-text', + title: 'Atomic-Commerce/Product Template Components/MultiValueText', + id: 'atomic-product-multi-value-text', + render: renderComponent, + parameters, + play, + args: { + 'attributes-field': 'cat_available_sizes', + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-multi-value-text', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], +}; + +export const WithDelimiter: Story = { + name: 'With delimiter', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-field': 'ec_product_id', + 'attributes-delimiter': '_', + }, +}; + +export const WithMaxValuesToDisplaySetToMinimum: Story = { + name: 'With max values to display set to minimum', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-max-values-to-display': 1, + }, +}; + +export const WithMaxValuesToDisplaySetToTotalNumberOfValues: Story = { + name: 'With max values to display set to total number of values', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-max-values-to-display': 6, + }, +}; + +export const InAPageWithTheCorrespondingFacet: Story = { + name: 'In a page with the corresponding facet', + decorators: [ + productDecorator, + commerceProductListDecorator, + (story) => { + return html` + + + + + ${story()} + + + `; + }, + commerceInterfaceDecorator, + ], +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss new file mode 100644 index 00000000000..d8afb1185ec --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss @@ -0,0 +1,19 @@ +:host { + > ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; + + li { + display: inline-block; + } + } +} + +.separator { + &::before { + display: inline; + content: ',\00a0'; + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx new file mode 100644 index 00000000000..8a6682d0869 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx @@ -0,0 +1,200 @@ +import { + BreadcrumbManager, + buildProductListing, + buildSearch, + Product, + ProductListing, + ProductTemplatesHelpers, + Search, + RegularFacetValue, +} from '@coveo/headless/commerce'; +import {Component, Element, Prop, h, State, VNode} from '@stencil/core'; +import {getFieldValueCaption} from '../../../../utils/field-utils'; +import {InitializeBindings} from '../../../../utils/initialization-utils'; +import {titleToKebab} from '../../../../utils/utils'; +import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; +import {ProductContext} from '../product-template-decorators'; + +/** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + * @part product-multi-value-text-list - The list of field values. + * @part product-multi-value-text-separator - The separator to display between each of the field values. + * @part product-multi-value-text-value - A field value. + * @part product-multi-value-text-value-more - A label indicating some values were omitted. + * @slot product-multi-value-text-value-* - A custom caption value that's specified for a given part of a multi-text field value. For example, if you want to use `Off-Campus Resident` as a caption value for `Off-campus apartment` in `Off-campus apartment;On-campus apartment`, you'd use `Off-Campus Resident`). The suffix of this slot corresponds with the field value, written in kebab case. + */ +@Component({ + tag: 'atomic-product-multi-value-text', + styleUrl: 'atomic-product-multi-value-text.pcss', + shadow: true, +}) +export class AtomicProductMultiValueText { + public breadcrumbManager!: BreadcrumbManager; + public searchOrListing!: Search | ProductListing; + + @InitializeBindings() public bindings!: CommerceBindings; + @ProductContext() private product!: Product; + + @Element() host!: HTMLElement; + + @State() public error!: Error; + + /** + * The field that the component should use. + * The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. + * Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + @Prop({reflect: true}) public field!: string; + + /** + * The maximum number of field values to display. + * If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + @Prop({reflect: true}) public maxValuesToDisplay = 3; + + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + @Prop({reflect: true}) public delimiter: string | null = null; + + private sortedValues: string[] | null = null; + + public initialize() { + if (this.bindings.interfaceElement.type === 'product-listing') { + this.searchOrListing = buildProductListing(this.bindings.engine); + } else { + this.searchOrListing = buildSearch(this.bindings.engine); + } + + this.breadcrumbManager = this.searchOrListing.breadcrumbManager(); + } + + private get productValues() { + const value = ProductTemplatesHelpers.getProductProperty( + this.product, + this.field + ); + + if (value === null) { + return null; + } + + if (Array.isArray(value)) { + return value.map((v) => `${v}`.trim()); + } + + if (typeof value !== 'string' || value.trim() === '') { + this.error = new Error( + `Could not parse "${value}" from field "${this.field}" as a string array.` + ); + return null; + } + + return this.delimiter + ? value.split(this.delimiter).map((value) => value.trim()) + : [value]; + } + + private get facetSelectedValues() { + return this.breadcrumbManager.state.facetBreadcrumbs + .filter((facet) => facet.field === this.field) + .reduce( + (values, facet) => [ + ...values, + ...facet.values.map(({value}) => (value as RegularFacetValue).value), + ], + [] as string[] + ); + } + + private updateSortedValues() { + const allValues = this.productValues; + if (allValues === null) { + this.sortedValues = null; + return; + } + const firstValues = this.facetSelectedValues.filter((value) => + allValues.includes(value) + ); + this.sortedValues = Array.from(new Set([...firstValues, ...allValues])); + } + + private getShouldDisplayLabel(values: string[]) { + return ( + this.maxValuesToDisplay > 0 && values.length > this.maxValuesToDisplay + ); + } + + private getNumberOfValuesToDisplay(values: string[]) { + return Math.min(values.length, this.maxValuesToDisplay); + } + + private renderValue(value: string) { + const label = getFieldValueCaption(this.field, value, this.bindings.i18n); + const kebabValue = titleToKebab(value); + return ( +
  • + + {label} + +
  • + ); + } + + private renderSeparator(beforeValue: string, afterValue: string) { + return ( + + ); + } + + private renderMoreLabel(value: number) { + return ( +
  • + {this.bindings.i18n.t('n-more', {value})} +
  • + ); + } + + private renderListItems(values: string[]) { + const numberOfValuesToDisplay = this.getNumberOfValuesToDisplay(values); + + const nodes: VNode[] = []; + for (let i = 0; i < numberOfValuesToDisplay; i++) { + if (i > 0) { + nodes.push(this.renderSeparator(values[i - 1], values[i])); + } + nodes.push(this.renderValue(values[i])); + } + if (this.getShouldDisplayLabel(values)) { + nodes.push( + this.renderSeparator( + values[numberOfValuesToDisplay - 1], + 'more-field-values' + ) + ); + nodes.push(this.renderMoreLabel(values.length - numberOfValuesToDisplay)); + } + return nodes; + } + + public componentWillRender() { + this.updateSortedValues(); + } + + public render() { + if (this.sortedValues === null) { + this.host.remove(); + return; + } + return ( +
      + {...this.renderListItems(this.sortedValues)} +
    + ); + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts new file mode 100644 index 00000000000..3ca01b158c1 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts @@ -0,0 +1,162 @@ +import {test, expect} from './fixture'; + +test.describe('default', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load(); + }); + + test('should render 3 values and 3 separators', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(3); + await expect(productMultiValueText.separators).toHaveCount(3); + }); + + test('should render an indicator that 3 more values are available', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.moreValuesIndicator(3)).toBeVisible(); + }); +}); + +test.describe('with a delimiter', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-delimiter', + }); + }); + test('when field value does not include the specified delimiter, should render as a single value', async ({ + productMultiValueText, + }) => { + await productMultiValueText.withCustomDelimiter({ + delimiter: '/', + field: 'ec_product_id', + values: ['a', 'b', 'c', 'd', 'e'], + }); + + await expect(productMultiValueText.values).toHaveCount(1); + await expect(productMultiValueText.separators).toHaveCount(0); + await expect(productMultiValueText.values.first()).toHaveText('a/b/c/d/e'); + await expect(productMultiValueText.moreValuesIndicator()).not.toBeVisible(); + }); + + test('when field value includes the specified delimiter, should render as distinct values', async ({ + productMultiValueText, + }) => { + await productMultiValueText.withCustomDelimiter({ + delimiter: '_', + field: 'ec_product_id', + values: ['a', 'b', 'c', 'd', 'e'], + }); + + await expect(productMultiValueText.values).toHaveCount(3); + await expect(productMultiValueText.separators).toHaveCount(3); + await expect(productMultiValueText.values.first()).toHaveText('a'); + await expect(productMultiValueText.values.nth(1)).toHaveText('b'); + await expect(productMultiValueText.values.nth(2)).toHaveText('c'); + await expect(productMultiValueText.moreValuesIndicator(2)).toBeVisible(); + }); +}); + +test.describe('with max-values-to-display set to minimum (1)', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-max-values-to-display-set-to-minimum', + }); + }); + + test('should render 1 value and 1 separator', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(1); + await expect(productMultiValueText.separators).toHaveCount(1); + }); + + test('should render an indicator that 5 more values are available', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.moreValuesIndicator(5)).toBeVisible(); + }); +}); + +test.describe('with max-values-to-display set to total number of values (6)', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-max-values-to-display-set-to-total-number-of-values', + }); + }); + + test('should be a11y compliant', async ({ + productMultiValueText, + makeAxeBuilder, + }) => { + await productMultiValueText.hydrated.waitFor(); + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations).toEqual([]); + }); + + test('should render 6 values and 5 separators', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(6); + await expect(productMultiValueText.separators).toHaveCount(5); + }); + + test('should not render an indicator that more values are available', async ({ + productMultiValueText, + }) => { + expect(productMultiValueText.moreValuesIndicator()).not.toBeVisible(); + }); +}); + +test.describe('in a page with corresponding facet', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'in-a-page-with-the-corresponding-facet', + }); + }); + + test('should be a11y compliant', async ({ + productMultiValueText, + makeAxeBuilder, + }) => { + await productMultiValueText.hydrated.waitFor(); + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations).toEqual([]); + }); + + test('with no selected values in corresponding facet, should render values in default order', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values.first()).toHaveText('XS'); + await expect(productMultiValueText.values.nth(1)).toHaveText('S'); + await expect(productMultiValueText.values.nth(2)).toHaveText('M'); + }); + + test('with a selected value in corresponding facet, should render that value first', async ({ + page, + productMultiValueText, + }) => { + await expect(productMultiValueText.values.first()).toHaveText('XS'); + + await page.getByLabel('Inclusion filter on L').click(); + + await expect(productMultiValueText.values.first()).toHaveText('L'); + }); + + test('with 3 selected values in corresponding facet, should render those values in alphabetical order', async ({ + productMultiValueText, + page, + }) => { + await page.getByLabel('Inclusion filter on M').click(); + await expect(page.getByText('Clear filter')).toBeVisible(); + await page.getByLabel('Inclusion filter on L').click(); + await expect(page.getByText('Clear 2 filters')).toBeVisible(); + await page.getByLabel('Inclusion filter on XL').click(); + await expect(page.getByText('Clear 3 filters')).toBeVisible(); + + await expect(productMultiValueText.values.first()).toHaveText('L'); + await expect(productMultiValueText.values.nth(1)).toHaveText('M'); + await expect(productMultiValueText.values.nth(2)).toHaveText('XL'); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts new file mode 100644 index 00000000000..7a714d4e8a2 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts @@ -0,0 +1,19 @@ +import {test as base} from '@playwright/test'; +import { + AxeFixture, + makeAxeBuilder, +} from '../../../../../../playwright-utils/base-fixture'; +import {ProductMultiValueTextPageObject} from './page-object'; + +type MyFixtures = { + productMultiValueText: ProductMultiValueTextPageObject; +}; + +export const test = base.extend({ + makeAxeBuilder, + productMultiValueText: async ({page}, use) => { + await use(new ProductMultiValueTextPageObject(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts new file mode 100644 index 00000000000..0762081d1a8 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts @@ -0,0 +1,48 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class ProductMultiValueTextPageObject extends BasePageObject<'atomic-product-multi-value-text'> { + constructor(page: Page) { + super(page, 'atomic-product-multi-value-text'); + } + + get values() { + return this.hydrated + .first() + .locator('li[part="product-multi-value-text-value"]'); + } + + get separators() { + return this.hydrated.first().locator('li[class="separator"]'); + } + + moreValuesIndicator(expectedNumber?: number) { + return this.hydrated + .first() + .getByText( + `${expectedNumber ? expectedNumber.toString() + ' ' : ''}more...` + ); + } + + async withCustomDelimiter({ + delimiter, + values, + field, + }: { + delimiter: string; + values: string[]; + field: string; + }) { + await this.page.route('**/commerce/v2/listing', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0][field] = values.join(delimiter); + await route.fulfill({ + response, + json: body, + }); + }); + + return this; + } +} diff --git a/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx b/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx index ca3ae3e4525..3a6f3578c79 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx +++ b/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx @@ -118,13 +118,7 @@ export class AtomicResultMultiText { } private getNumberOfValuesToDisplay(values: string[]) { - if (values.length <= this.maxValuesToDisplay) { - return values.length; - } - if (this.maxValuesToDisplay < 2) { - return this.maxValuesToDisplay; - } - return Math.min(values.length - 2, this.maxValuesToDisplay); + return Math.min(values.length, this.maxValuesToDisplay); } private renderValue(value: string) { diff --git a/packages/atomic/src/pages/examples/commerce-website/cart.html b/packages/atomic/src/pages/examples/commerce-website/cart.html index 2c5185bf747..746f2c2edba 100644 --- a/packages/atomic/src/pages/examples/commerce-website/cart.html +++ b/packages/atomic/src/pages/examples/commerce-website/cart.html @@ -55,6 +55,7 @@

    Cart

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/homepage.html b/packages/atomic/src/pages/examples/commerce-website/homepage.html index 1c7ef5ed9f0..120d16e0eb8 100644 --- a/packages/atomic/src/pages/examples/commerce-website/homepage.html +++ b/packages/atomic/src/pages/examples/commerce-website/homepage.html @@ -59,6 +59,7 @@

    HOMEPAGE

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-pants.html b/packages/atomic/src/pages/examples/commerce-website/listing-pants.html index b11092c8962..3c27803906e 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-pants.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-pants.html @@ -52,6 +52,7 @@

    Pants

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html b/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html index b4a3e82f08d..030f6052c4d 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html @@ -52,6 +52,7 @@

    Surf accessories

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-towels.html b/packages/atomic/src/pages/examples/commerce-website/listing-towels.html index 05390d8f402..b79cc5f6657 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-towels.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-towels.html @@ -52,6 +52,7 @@

    Towels

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/search.html b/packages/atomic/src/pages/examples/commerce-website/search.html index 010027eebef..30d48c178f4 100644 --- a/packages/atomic/src/pages/examples/commerce-website/search.html +++ b/packages/atomic/src/pages/examples/commerce-website/search.html @@ -47,6 +47,11 @@

    Search page

    + + + @@ -102,9 +107,17 @@

    Search page

    + + + + + +