Skip to content

Commit

Permalink
fix: Add cart context check for cart bundle and issue component (SAP#…
Browse files Browse the repository at this point in the history
…12322)

This commit introduces checks for the cart context in 2 components that allow to edit cart bound configurations. Editing configurations must not be possible in case the cart entry is part of a saved cart.

Closes SAP#12315
  • Loading branch information
ChristophHi authored May 6, 2021
1 parent bcb0854 commit 56c52dd
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@
</ng-container>
<cx-configure-cart-entry
*ngIf="
orderEntry?.product?.configurable && quantityControl$
| async as quantityControl
(shouldShowButton$ | async) &&
orderEntry?.product?.configurable &&
quantityControl$ | async as quantityControl
"
[cartEntry]="orderEntry"
[readOnly]="readonly$ | async"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ChangeDetectorRef, Pipe, PipeTransform, Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlContainer, FormControl } from '@angular/forms';
import { I18nTestingModule, OrderEntry } from '@spartacus/core';
import {
I18nTestingModule,
OrderEntry,
PromotionLocation,
} from '@spartacus/core';
import {
CommonConfiguratorTestUtilsService,
CommonConfiguratorUtilsService,
Expand All @@ -10,7 +14,7 @@ import {
ConfiguratorType,
} from '@spartacus/product-configurator/common';
import { BreakpointService, CartItemContext } from '@spartacus/storefront';
import { of, ReplaySubject } from 'rxjs';
import { BehaviorSubject, of, ReplaySubject } from 'rxjs';
import { take, toArray } from 'rxjs/operators';
import { ConfiguratorCartEntryBundleInfoComponent } from './configurator-cart-entry-bundle-info.component';

Expand All @@ -27,6 +31,9 @@ class MockCartItemContext implements Partial<CartItemContext> {
item$ = new ReplaySubject<OrderEntry>(1);
readonly$ = new ReplaySubject<boolean>(1);
quantityControl$ = new ReplaySubject<FormControl>(1);
location$ = new BehaviorSubject<PromotionLocation>(
PromotionLocation.SaveForLater
);
}

const configurationInfos: ConfigurationInfo[] = [
Expand Down Expand Up @@ -271,6 +278,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => {
configurable: true,
},
});
mockCartItemContext.location$.next(PromotionLocation.ActiveCart);
mockCartItemContext.readonly$.next(false);
mockCartItemContext.quantityControl$.next(new FormControl());
fixture.detectChanges();
Expand Down Expand Up @@ -316,6 +324,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => {
configurable: true,
},
});
mockCartItemContext.location$.next(PromotionLocation.ActiveCart);
mockCartItemContext.readonly$.next(false);
mockCartItemContext.quantityControl$.next(new FormControl());
fixture.detectChanges();
Expand Down Expand Up @@ -638,5 +647,43 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => {
);
});
});

describe('shouldShowButton', () => {
beforeEach(() => {
const quantityControl = new FormControl();
mockCartItemContext.quantityControl$?.next(quantityControl);
mockCartItemContext.item$?.next({
product: { configurable: true },
configurationInfos: [
{
configurationLabel: 'Canon ABC',
configurationValue: '10',
configuratorType: ConfiguratorType.CPQ,
status: 'SUCCESS',
},
],
});
});
it('should prevent the rendering of "edit configuration" if context is SaveForLater', () => {
mockCartItemContext.location$?.next(PromotionLocation.SaveForLater);
fixture.detectChanges();

const htmlElem = fixture.nativeElement;
expect(
htmlElem.querySelectorAll('.cx-configure-cart-entry').length
).toBe(0);
});

it('should allow the rendering of "edit configuration" if context is active cart', () => {
mockCartItemContext.location$?.next(PromotionLocation.ActiveCart);

fixture.detectChanges();

const htmlElem = fixture.nativeElement;
expect(
htmlElem.querySelectorAll('cx-configure-cart-entry').length
).toBe(1);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ export class ConfiguratorCartEntryBundleInfoComponent {
isDesktop(): Observable<boolean> {
return this.breakpointService?.isUp(BREAKPOINT.md);
}

// TODO: remove the logic below when configurable products support "Saved Cart" and "Save For Later"
readonly shouldShowButton$: Observable<boolean> = this.commonConfigUtilsService.isActiveCartContext(
this.cartItemContext
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,22 +196,9 @@ describe('ConfiguratorCartEntryInfoComponent', () => {
});

describe('shouldShowButton', () => {
it('should emit false if context is SaveForLater', () => {
mockCartItemContext.location$.next(PromotionLocation.SaveForLater);
fixture.detectChanges();

let result: boolean | undefined;

component.shouldShowButton$
.subscribe((data) => (result = data))
.unsubscribe();

expect(result).toEqual(false);
});

it('should prevent the rendering of "edit configuration" if context is SaveForLater', () => {
beforeEach(() => {
const quantityControl = new FormControl();
mockCartItemContext.location$.next(PromotionLocation.SaveForLater);

mockCartItemContext.quantityControl$.next(quantityControl);
mockCartItemContext.item$.next({
statusSummaryList: undefined,
Expand All @@ -222,6 +209,9 @@ describe('ConfiguratorCartEntryInfoComponent', () => {
},
],
});
});
it('should prevent the rendering of "edit configuration" if context is SaveForLater', () => {
mockCartItemContext.location$.next(PromotionLocation.SaveForLater);
fixture.detectChanges();

const htmlElem = fixture.nativeElement;
Expand All @@ -230,44 +220,8 @@ describe('ConfiguratorCartEntryInfoComponent', () => {
).toBe(0);
});

it('should emit false if context is SavedCart', () => {
mockCartItemContext.location$.next(PromotionLocation.SavedCart);
fixture.detectChanges();
let result: boolean | undefined;

component.shouldShowButton$
.subscribe((data) => (result = data))
.unsubscribe();

expect(result).toEqual(false);
});

it('should emit true if context is NOT related to saved carts ', () => {
mockCartItemContext.location$.next(PromotionLocation.ActiveCart);

fixture.detectChanges();
let result: boolean | undefined;

component.shouldShowButton$
.subscribe((data) => (result = data))
.unsubscribe();

expect(result).toEqual(true);
});

it('should allow the rendering of "edit configuration" if context is active cart', () => {
const quantityControl = new FormControl();
mockCartItemContext.location$.next(PromotionLocation.ActiveCart);
mockCartItemContext.quantityControl$.next(quantityControl);
mockCartItemContext.item$.next({
statusSummaryList: undefined,
product: { configurable: true },
configurationInfos: [
{
configuratorType: ConfiguratorType.VARIANT,
},
],
});
fixture.detectChanges();

const htmlElem = fixture.nativeElement;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Component, Optional } from '@angular/core';
import { FormControl } from '@angular/forms';
import { OrderEntry, PromotionLocation } from '@spartacus/core';
import { OrderEntry } from '@spartacus/core';
import { CartItemContext } from '@spartacus/storefront';
import { EMPTY, Observable } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { CommonConfiguratorUtilsService } from '../../shared/utils/common-configurator-utils.service';
import { map } from 'rxjs/operators';

@Component({
selector: 'cx-configurator-cart-entry-info',
Expand Down Expand Up @@ -45,15 +44,10 @@ export class ConfiguratorCartEntryInfoComponent {
this.cartItemContext?.readonly$ ?? EMPTY;

// TODO: remove the logic below when configurable products support "Saved Cart" and "Save For Later"
readonly shouldShowButton$: Observable<boolean> = (
this.cartItemContext?.location$ ?? EMPTY
).pipe(
map(
(location) =>
location !== PromotionLocation.SaveForLater &&
location !== PromotionLocation.SavedCart
)
);
readonly shouldShowButton$: Observable<boolean> = this
.commonConfigUtilsService
? this.commonConfigUtilsService.isActiveCartContext(this.cartItemContext)
: of(true);

/**
* Verifies whether the configuration infos have any entries and the first entry has a status.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
<cx-configure-cart-entry
class="cx-error-msg-action"
*ngIf="
orderEntry?.product?.configurable && quantityControl$
| async as quantityControl
(shouldShowButton$ | async) &&
orderEntry?.product?.configurable &&
quantityControl$ | async as quantityControl
"
[cartEntry]="orderEntry"
[readOnly]="readonly$ | async"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormControl } from '@angular/forms';
import { OrderEntry } from '@spartacus/core';
import { OrderEntry, PromotionLocation } from '@spartacus/core';
import { CartItemContext, CartItemContextSource } from '@spartacus/storefront';
import { ReplaySubject } from 'rxjs';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { take, toArray } from 'rxjs/operators';
import {
ConfigurationInfo,
Expand All @@ -23,6 +23,9 @@ class MockCartItemContext implements Partial<CartItemContext> {
item$ = new ReplaySubject<OrderEntry>(1);
readonly$ = new ReplaySubject<boolean>(1);
quantityControl$ = new ReplaySubject<FormControl>(1);
location$ = new BehaviorSubject<PromotionLocation>(
PromotionLocation.ActiveCart
);
}

describe('ConfigureIssuesNotificationComponent', () => {
Expand Down Expand Up @@ -155,4 +158,37 @@ describe('ConfigureIssuesNotificationComponent', () => {
htmlElem.innerHTML
);
});

describe('shouldShowButton', () => {
beforeEach(() => {
const quantityControl = new FormControl();

mockCartItemContext.quantityControl$?.next(quantityControl);
mockCartItemContext.item$?.next({
statusSummaryList: [
{ numberOfIssues: 2, status: OrderEntryStatus.Error },
],
product: { configurable: true },
});
});
it('should prevent the rendering of "edit configuration" if context is SaveForLater', () => {
mockCartItemContext.location$?.next(PromotionLocation.SaveForLater);
fixture.detectChanges();

const htmlElem = fixture.nativeElement;
expect(htmlElem.querySelectorAll('.cx-configure-cart-entry').length).toBe(
0
);
});

it('should allow the rendering of "edit configuration" if context is active cart', () => {
mockCartItemContext.location$?.next(PromotionLocation.ActiveCart);
fixture.detectChanges();

const htmlElem = fixture.nativeElement;
expect(htmlElem.querySelectorAll('cx-configure-cart-entry').length).toBe(
1
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export class ConfiguratorIssuesNotificationComponent {
readonly readonly$: Observable<boolean> =
this.cartItemContext?.readonly$ ?? EMPTY;

// TODO: remove the logic below when configurable products support "Saved Cart" and "Save For Later"
readonly shouldShowButton$: Observable<boolean> = this.commonConfigUtilsService.isActiveCartContext(
this.cartItemContext
);

/**
* Verifies whether the item has any issues.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Type } from '@angular/core';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { FormControl } from '@angular/forms';
import {
Cart,
OCC_USER_ID_ANONYMOUS,
OrderEntry,
PromotionLocation,
UserIdService,
} from '@spartacus/core';
import { Observable, of } from 'rxjs';
import { CartItemContext, CartItemContextSource } from '@spartacus/storefront';
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
import {
CommonConfigurator,
ConfiguratorType,
Expand Down Expand Up @@ -38,8 +41,18 @@ class MockUserIdService {
}
}

class MockCartItemContext implements Partial<CartItemContext> {
item$ = new ReplaySubject<OrderEntry>(1);
readonly$ = new ReplaySubject<boolean>(1);
quantityControl$ = new ReplaySubject<FormControl>(1);
location$ = new BehaviorSubject<PromotionLocation>(
PromotionLocation.ActiveCart
);
}

describe('CommonConfiguratorUtilsService', () => {
let classUnderTest: CommonConfiguratorUtilsService;
let mockCartItemContext: CartItemContextSource;

beforeEach(
waitForAsync(() => {
Expand All @@ -49,6 +62,7 @@ describe('CommonConfiguratorUtilsService', () => {
provide: UserIdService,
useClass: MockUserIdService,
},
{ provide: CartItemContext, useClass: MockCartItemContext },
],
}).compileComponents();
})
Expand All @@ -58,6 +72,7 @@ describe('CommonConfiguratorUtilsService', () => {
CommonConfiguratorUtilsService as Type<CommonConfiguratorUtilsService>
);
owner = ConfiguratorModelUtils.createInitialOwner();
mockCartItemContext = TestBed.inject(CartItemContext) as any;
cartItem = {};
});

Expand Down Expand Up @@ -241,4 +256,32 @@ describe('CommonConfiguratorUtilsService', () => {
).toBe(true);
});
});

describe('isActiveCartContext', () => {
it('should emit false if context is SaveForLater', () => {
mockCartItemContext.location$?.next(PromotionLocation.SaveForLater);

let result: boolean | undefined;

classUnderTest
.isActiveCartContext(mockCartItemContext)
.subscribe((data) => (result = data))
.unsubscribe();

expect(result).toEqual(false);
});

it('should emit true if context is active cart', () => {
mockCartItemContext.location$?.next(PromotionLocation.ActiveCart);

let result: boolean | undefined;

classUnderTest
.isActiveCartContext(mockCartItemContext)
.subscribe((data) => (result = data))
.unsubscribe();

expect(result).toEqual(true);
});
});
});
Loading

0 comments on commit 56c52dd

Please sign in to comment.