diff --git a/libs/domain/product/src/mocks/src/mock-product.service.ts b/libs/domain/product/src/mocks/src/mock-product.service.ts index f8612230..e44c0e54 100644 --- a/libs/domain/product/src/mocks/src/mock-product.service.ts +++ b/libs/domain/product/src/mocks/src/mock-product.service.ts @@ -100,6 +100,15 @@ export class MockProductService implements Partial { availability: true, }, categoryIds: ['category id 1'], + variants: { + 'variant-1': { brand: 'Brand1', color: 'color1' }, + 'variant-2': { brand: 'Brand1', color: 'green' }, + 'variant-3': { brand: 'Brand1', color: 'blue' }, + }, + variantDefinition: { + brand: ['Brand1'], + color: ['color1', 'green', 'blue'], + }, }, { sku: '2', @@ -138,6 +147,14 @@ export class MockProductService implements Partial { availability: false, }, categoryIds: ['category id 2'], + variantDefinition: { + brand: ['Brand2'], + color: ['red', 'green', 'blue'], + }, + variants: { + 'variant-1': { brand: 'Brand2', color: 'color2' }, + 'variant-2': { brand: 'Brand3' }, + } }, { sku: '3', @@ -291,7 +308,6 @@ export class MockProductService implements Partial { 'Sample attribute lengthy name, Sample attribute lengthy name, Sample attribute lengthy name.', }, }, - { sku: 'single-image', name: 'Sample product with one image', @@ -326,6 +342,59 @@ export class MockProductService implements Partial { discontinued: true, discontinuedNote: 'This product is discontinued...', }, + { + sku: 'variant-selector', + name: 'Product with Variants', + mediaSet, + description: 'This product has multiple variants to choose from.', + price: { + defaultPrice: { + currency: 'EUR', + value: 1999, + isNet: true, + }, + originalPrice: { + currency: 'EUR', + value: 2499, + isNet: true, + }, + }, + averageRating: 4.5, + reviewCount: 10, + attributes: { + brand: 'Brand9', + color: 'red', + size: 'M', + }, + attributeNames: { + brand: 'Brand', + color: 'Color', + size: 'Size', + }, + labels: [newLabel, saleLabel], + availability: { + quantity: 5, + isNeverOutOfStock: false, + availability: true, + }, + categoryIds: ['category id 9'], + variants: { + 'variant-1': { brand: 'Brand9', color: 'red', size: 'S' }, + 'variant-2': { brand: 'Brand9', color: 'red', size: 'M' }, + 'variant-3': { brand: 'Brand9', color: 'red', size: 'L' }, + 'variant-4': { brand: 'Brand9', color: 'green', size: 'S' }, + 'variant-5': { brand: 'Brand9', color: 'green', size: 'M' }, + 'variant-6': { brand: 'Brand9', color: 'green', size: 'L' }, + 'variant-7': { brand: 'Brand9', color: 'blue', size: 'S' }, + 'variant-8': { brand: 'Brand9', color: 'blue', size: 'M' }, + 'variant-9': { brand: 'Brand9', color: 'blue', size: 'L' }, + }, + variantDefinition: { + brand: ['Brand9'], + color: ['red', 'green', 'blue'], + size: ['S', 'M', 'L'], + }, + } ]; get(qualifier: ProductQualifier): Observable { diff --git a/libs/domain/product/src/services/adapter/normalizers/product/product.normalizer.ts b/libs/domain/product/src/services/adapter/normalizers/product/product.normalizer.ts index f7a0018d..6b122c5b 100644 --- a/libs/domain/product/src/services/adapter/normalizers/product/product.normalizer.ts +++ b/libs/domain/product/src/services/adapter/normalizers/product/product.normalizer.ts @@ -138,6 +138,10 @@ export function productVariantNormalizer( const variantMap = abstract.attributeMap?.attributeVariantMap; const variantDefinition = abstract.attributeMap?.superAttributes; + if (!variantDefinition) { + return {}; + } + if (variantMap) { const variantMapEntries = Object.entries(variantMap); diff --git a/libs/domain/product/variant-selector/stories/demo.stories.ts b/libs/domain/product/variant-selector/stories/demo.stories.ts new file mode 100644 index 00000000..aa2d8613 --- /dev/null +++ b/libs/domain/product/variant-selector/stories/demo.stories.ts @@ -0,0 +1,19 @@ +import { MockProductService } from '@oryx-frontend/product/mocks'; +import { Meta, Story } from '@storybook/web-components'; +import { TemplateResult, html } from 'lit'; +import { storybookPrefix } from '../../.constants'; + +export default { + title: `${storybookPrefix}/Variant selector`, + parameters: { + chromatic: { + disableSnapshot: true, + }, + }, +} as Meta; + +const Template: Story = (): TemplateResult => { + return html``; +}; + +export const Demo = Template.bind({}); diff --git a/libs/domain/product/variant-selector/stories/static.stories.ts b/libs/domain/product/variant-selector/stories/static.stories.ts new file mode 100644 index 00000000..512f9e31 --- /dev/null +++ b/libs/domain/product/variant-selector/stories/static.stories.ts @@ -0,0 +1,26 @@ +import { MockProductService } from '@oryx-frontend/product/mocks'; +import { Meta, Story } from '@storybook/web-components'; +import { TemplateResult, html } from 'lit'; +import { storybookPrefix } from '../../.constants'; + +export default { + title: `${storybookPrefix}/Variant selector/Static`, +} as unknown as Meta; + +const Template: Story = (): TemplateResult => { + return html` +
+

3 variants:

+ +
+
+

2 variants:

+ +
+
+

Color variants disabled:

+ +
+ `; +} +export const Variations = Template.bind({}); diff --git a/libs/domain/product/variant-selector/variant-selector.component.spec.ts b/libs/domain/product/variant-selector/variant-selector.component.spec.ts new file mode 100644 index 00000000..66dd85b6 --- /dev/null +++ b/libs/domain/product/variant-selector/variant-selector.component.spec.ts @@ -0,0 +1,63 @@ +import { fixture } from '@open-wc/testing-helpers'; +import { createInjector, destroyInjector } from '@oryx-frontend/di'; +import { mockProductProviders, MockProductService } from '@oryx-frontend/product/mocks'; +import { useComponent } from '@oryx-frontend/utilities'; +import { html } from 'lit'; +import { ProductVariantSelectorComponent } from './variant-selector.component'; +import { productVariantSelectorComponent } from './variant-selector.def'; +import { ProductService } from '@oryx-frontend/product'; +import { ContextService, DefaultContextService } from '@oryx-frontend/core'; +import { beforeEach } from 'vitest'; + +describe('ProductVariantSelectorComponent', () => { + let element: ProductVariantSelectorComponent; + let productService: MockProductService; + + beforeAll(async () => { + await useComponent(productVariantSelectorComponent); + }); + + beforeEach(async () => { + const injector = createInjector({ + providers: [ + { + provide: ProductService, + useClass: MockProductService + }, + { + provide: ContextService, + useClass: DefaultContextService, + }, + ], + }); + + productService = injector.inject(ProductService); + + + }); + + afterEach(() => { + destroyInjector(); + }); + + describe('when the component is created', () => { + beforeEach(async () => { + element = await fixture( + html` + ` + ); + }); + + it('should be defined', () => { + expect(element).toBeInstanceOf(ProductVariantSelectorComponent); + }); + + it('passes the a11y audit', async () => { + expect(element).shadowDom.to.be.accessible(); + }); + + it('should render a toggle variants', () => { + expect(element).toContainElement('oryx-toggle-icon'); + }); + }); +}); diff --git a/libs/domain/product/variant-selector/variant-selector.component.ts b/libs/domain/product/variant-selector/variant-selector.component.ts index 8c67159c..855712bc 100644 --- a/libs/domain/product/variant-selector/variant-selector.component.ts +++ b/libs/domain/product/variant-selector/variant-selector.component.ts @@ -23,9 +23,6 @@ export class ProductVariantSelectorComponent extends ProductMixin( ) { static styles = variantListStyle; - protected productListService = resolve(ProductListService); - protected productListPageService = resolve(ProductListPageService); - protected routerService = resolve(RouterService); protected linkService = resolve(LinkService); @@ -48,6 +45,9 @@ export class ProductVariantSelectorComponent extends ProductMixin( }); protected render(): TemplateResult | void { + console.log('this.$product():'); + + const variants = this.$product()?.variants; if (!variants || Object.keys(variants).length < 2) return; @@ -57,6 +57,7 @@ export class ProductVariantSelectorComponent extends ProductMixin( protected renderAttributeSelectors() { const { variantDefinition, attributeNames } = this.$product() ?? {}; + if (!variantDefinition) return; const keys = Object.keys(variantDefinition); @@ -71,7 +72,7 @@ export class ProductVariantSelectorComponent extends ProductMixin( ${value} ` - )}` + )}` )} `; } @@ -123,38 +124,6 @@ export class ProductVariantSelectorComponent extends ProductMixin( return isVariantDisabled; } - // protected isDisabled( - // attributeKey: string, - // attributeValue: string, - // index: number - // ): boolean { - // const product = this.$product(); - - // if (!product?.variants) { - // return false; - // } - - // // Extract active variant attribute keys up to the given index - // const activeKeys = Object.keys(product.variantDefinition || {}).slice( - // 0, - // index - // ); - - // // Check if any variant matches the selected attribute values - // const isVariantDisabled = !Object.values(product.variants).some( - // (variant) => { - // // Check if all selected attribute values match the variant - // return ( - // activeKeys.every( - // (key) => variant[key] === this.$product()?.attributes?.[key] - // ) && variant[attributeKey] === attributeValue - // ); - // } - // ); - - // return isVariantDisabled; - // } - protected handleVariantChange(e: Event) { const form = e.currentTarget as HTMLFormElement; const formData = new FormData(form);