Skip to content

Commit

Permalink
refactor: rework product-item-variations component (#1648)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The `ish-product-item-variations` component has been refactored.
  • Loading branch information
dhhyi authored and shauke committed May 23, 2024
1 parent 48fc8f7 commit a8c94a0
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 114 deletions.
1 change: 1 addition & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ For that reason product variations are now loaded lazily through the following c

- The `variations` property on the product view interface was removed. Variations can now be retrieved via the product context facade or the shopping facade.
- The `productMaster` property on the product view model has been removed. The master product should be individually retrieved.
- The `ish-product-item-variations` component has been refactored.

## From 5.0 to 5.1

Expand Down
13 changes: 13 additions & 0 deletions src/app/core/utils/dev/html-query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ export function findAllCustomElements(el: HTMLElement): string[] {
return returnList;
}

export function findAllElements(el: HTMLElement): string[] {
const returnList = [];
const tagList = getAllElementTagsRecursively(el);

for (const element of tagList) {
const tagName = element.toLocaleLowerCase();
returnList.push(tagName);
}

// leave out first testing div
return returnList.slice(1);
}

export function findAllDataTestingIDs(fixture: ComponentFixture<unknown>): string[] {
return fixture.debugElement.queryAll(By.css('[data-testing-id]')).map(el => el.attributes['data-testing-id']);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
<ng-container *ngIf="variationCount$ | async as variationCount; else variationProduct">
<span class="product-variation read-only">{{ variationCount }} {{ 'product.variations.text' | translate }}</span>
<ng-container *ngIf="isMasterProduct$ | async">
<span class="product-variation read-only">
{{ 'product.variations.text' | translate : { '0': variationCount$ | async } }}
</span>
</ng-container>
<ng-template #variationProduct>
<div *ngIf="visible$ | async" class="product-variation-container">
<ng-container
*ngIf="
'preferences.ChannelPreferences.EnableAdvancedVariationHandling' | ishServerSetting;
else normalVariationDisplay
"
>
<ish-product-variation-display />
</ng-container>
<ng-template #normalVariationDisplay>
<ng-container *ngIf="isVariationProduct$ | async">
<ng-container *ngIf="visible$ | async">
<div class="product-variation-container">
<ng-container *ngIf="readOnly$ | async; else variationSelect">
<ish-product-variation-display />
</ng-container>
</ng-template>
</div>

<ng-template #variationSelect>
<ish-product-variation-select />
</ng-template>
</ng-template>
<ng-template #variationSelect>
<ish-product-variation-select />
</ng-template>
</div>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';
import { TranslateCompiler, TranslateModule, TranslateService } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { BehaviorSubject, map } from 'rxjs';
import { anyString, instance, mock, when } from 'ts-mockito';

import { AppFacade } from 'ish-core/facades/app.facade';
import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ProductView } from 'ish-core/models/product-view/product-view.model';
import { ServerSettingPipe } from 'ish-core/pipes/server-setting.pipe';
import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils';
import { findAllElements } from 'ish-core/utils/dev/html-query-utils';
import { PWATranslateCompiler } from 'ish-core/utils/translate/pwa-translate-compiler';
import { ProductVariationDisplayComponent } from 'ish-shared/components/product/product-variation-display/product-variation-display.component';
import { ProductVariationSelectComponent } from 'ish-shared/components/product/product-variation-select/product-variation-select.component';

Expand All @@ -18,99 +19,116 @@ describe('Product Item Variations Component', () => {
let fixture: ComponentFixture<ProductItemVariationsComponent>;
let element: HTMLElement;
let context: ProductContextFacade;
let translate: TranslateService;

async function prepareTestbed(serverSetting: boolean) {
const readOnly$ = new BehaviorSubject<boolean>(false);
const visible$ = new BehaviorSubject<boolean>(true);
const advancedVariationHandling$ = new BehaviorSubject<boolean>(true);
const productType$ = new BehaviorSubject<'VariationProduct' | 'VariationProductMaster' | 'Product'>(
'VariationProduct'
);
const variationCount$ = new BehaviorSubject<number>(25);

beforeEach(async () => {
context = mock(ProductContextFacade);
when(context.select('displayProperties', 'variations')).thenReturn(of(true));
when(context.select('product')).thenReturn(of({ type: 'VariationProduct' } as ProductView));
when(context.select('displayProperties', 'variations')).thenReturn(visible$);
when(context.select('displayProperties', 'readOnly')).thenReturn(readOnly$);
when(context.select('product')).thenReturn(productType$.pipe(map(type => ({ type } as ProductView))));
when(context.select('variationCount')).thenReturn(variationCount$);

const appFacade = mock(AppFacade);
when(appFacade.serverSetting$(anyString())).thenReturn(advancedVariationHandling$);

await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
imports: [
TranslateModule.forRoot({
compiler: { provide: TranslateCompiler, useClass: PWATranslateCompiler },
}),
],
declarations: [
MockComponent(ProductVariationDisplayComponent),
MockComponent(ProductVariationSelectComponent),
MockPipe(ServerSettingPipe, () => serverSetting),
ProductItemVariationsComponent,
],
providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }],
providers: [
{ provide: AppFacade, useFactory: () => instance(appFacade) },
{ provide: ProductContextFacade, useFactory: () => instance(context) },
],
}).compileComponents();
}

describe('advanced variation handling', () => {
beforeEach(async () => {
prepareTestbed(true);
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductItemVariationsComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should render display for variation product in readOnly mode', () => {
when(context.select('displayProperties', 'readOnly')).thenReturn(of(true));
fixture.detectChanges();

expect(findAllCustomElements(element)).toMatchInlineSnapshot(`
[
"ish-product-variation-display",
]
`);
expect(element?.textContent).toBeEmpty();
});

it('should render display for variation product', () => {
fixture.detectChanges();

expect(findAllCustomElements(element)).toMatchInlineSnapshot(`
[
"ish-product-variation-display",
]
`);
expect(element?.textContent).toBeEmpty();
});

it('should render variation count for variation product master', () => {
when(context.select('product')).thenReturn(of({ type: 'VariationProductMaster' } as ProductView));
when(context.select('variationCount')).thenReturn(of(1234));
fixture.detectChanges();

expect(element?.textContent).toContain('1234');
});
});

describe('b2c variation handling', () => {
beforeEach(async () => {
prepareTestbed(false);
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductItemVariationsComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should render select for variation product', () => {
fixture.detectChanges();

expect(findAllCustomElements(element)).toMatchInlineSnapshot(`
[
"ish-product-variation-select",
]
`);
expect(element?.textContent).toBeEmpty();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProductItemVariationsComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

translate = TestBed.inject(TranslateService);
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should display variation count for masters', () => {
productType$.next('VariationProductMaster');
readOnly$.next(true);
visible$.next(true);
advancedVariationHandling$.next(true);
variationCount$.next(25);

translate.setDefaultLang('en');
translate.use('en');
translate.set('product.variations.text', '{{ 0, plural, =1{one model} other{# models} }}');

fixture.detectChanges();

expect(findAllElements(element)).toEqual(['span']);
expect(element.textContent).toMatchInlineSnapshot(`" 25 models "`);

variationCount$.next(1);
fixture.detectChanges();

expect(element.textContent).toMatchInlineSnapshot(`" one model "`);
});

describe.each`
advancedVariationHandling | productType | readOnly | expectedElements
${true} | ${'VariationProduct'} | ${false} | ${['div', 'ish-product-variation-display']}
${true} | ${'VariationProduct'} | ${true} | ${['div', 'ish-product-variation-display']}
${true} | ${'VariationProductMaster'} | ${false} | ${['span']}
${true} | ${'VariationProductMaster'} | ${true} | ${['span']}
${true} | ${'Product'} | ${false} | ${[]}
${true} | ${'Product'} | ${true} | ${[]}
${false} | ${'VariationProduct'} | ${false} | ${['div', 'ish-product-variation-select']}
${false} | ${'VariationProduct'} | ${true} | ${['div', 'ish-product-variation-display']}
${false} | ${'VariationProductMaster'} | ${false} | ${['span']}
${false} | ${'VariationProductMaster'} | ${true} | ${['span']}
${false} | ${'Product'} | ${false} | ${[]}
${false} | ${'Product'} | ${true} | ${[]}
`(
`with advancedVariationHandling=$advancedVariationHandling, productType=$productType, readOnly=$readOnly`,
({ advancedVariationHandling, productType, readOnly, expectedElements }) => {
beforeEach(() => {
advancedVariationHandling$.next(advancedVariationHandling);
productType$.next(productType);
readOnly$.next(readOnly);
visible$.next(true);
fixture.detectChanges();
});

it(`should ${expectedElements?.length ? `display ${expectedElements.join()}` : 'not display anything'}`, () => {
expect(findAllElements(element)).toEqual(expectedElements);
});

it('should not display anything if variations are not visible except for VariationProductMaster', () => {
visible$.next(false);
fixture.detectChanges();
productType === 'VariationProductMaster'
? expect(findAllElements(element)).toEqual(['span'])
: expect(findAllElements(element)).toBeEmpty();
});
}
);
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Observable, combineLatest } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

import { AppFacade } from 'ish-core/facades/app.facade';
import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { ProductHelper } from 'ish-core/models/product/product.helper';

@Component({
selector: 'ish-product-item-variations',
Expand All @@ -12,12 +15,25 @@ export class ProductItemVariationsComponent implements OnInit {
visible$: Observable<boolean>;
readOnly$: Observable<boolean>;
variationCount$: Observable<number>;
isMasterProduct$: Observable<boolean>;
isVariationProduct$: Observable<boolean>;

constructor(private context: ProductContextFacade) {}
constructor(private context: ProductContextFacade, private appFacade: AppFacade) {}

ngOnInit() {
this.visible$ = this.context.select('displayProperties', 'variations');
this.readOnly$ = this.context.select('displayProperties', 'readOnly');

const advancedVariationHandling$ = this.appFacade.serverSetting$<boolean>(
'preferences.ChannelPreferences.EnableAdvancedVariationHandling'
);
this.readOnly$ = combineLatest([
this.context.select('displayProperties', 'readOnly').pipe(startWith(false)),
advancedVariationHandling$,
]).pipe(map(([readOnly, advancedVariationHandling]) => readOnly || advancedVariationHandling));

this.isMasterProduct$ = this.context.select('product').pipe(map(ProductHelper.isMasterProduct));
this.isVariationProduct$ = this.context.select('product').pipe(map(ProductHelper.isVariationProduct));

this.variationCount$ = this.context.select('variationCount');
}
}
2 changes: 1 addition & 1 deletion src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@
"product.reviews.your_review.heading": "Ihre Produktbewertung",
"product.short_description.label": "Kurzbeschreibung",
"product.specific_attributes.label": "Spezifische Attribute",
"product.variations.text": "Variationen",
"product.variations.text": "{{ 0, plural, zero{keine Variationen} one{eine Variation} other{# Variationen}}}",
"product.warranty.code.text": "Garantie-Code:",
"product.warranty.detail.text": "Details",
"product.warranty.expire_date.text": "Läuft aus am:",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@
"product.reviews.your_review.heading": "Your product review",
"product.short_description.label": "Short description",
"product.specific_attributes.label": "Specific attributes",
"product.variations.text": "Variations",
"product.variations.text": "{{ 0, plural, one{# Variation} other{# Variations}}}",
"product.warranty.code.text": "Warranty code:",
"product.warranty.detail.text": "Details",
"product.warranty.expire_date.text": "Expires:",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/i18n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@
"product.reviews.your_review.heading": "Votre avis sur le produit",
"product.short_description.label": "Description courte",
"product.specific_attributes.label": "Attributs spécifiques",
"product.variations.text": "Variations",
"product.variations.text": "{{ 0, plural, one{# Variation} other{# Variations}}}",
"product.warranty.code.text": "Code de garantie",
"product.warranty.detail.text": "Détails",
"product.warranty.expire_date.text": "Expire le :",
Expand Down

0 comments on commit a8c94a0

Please sign in to comment.