Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: addToCart takes actual product code into account #18731

Merged
merged 24 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6770861
fix: addToCart takes actual product code into account
ChristophHi Apr 16, 2024
5437a28
Remove assertion
ChristophHi Apr 16, 2024
c893f2a
Introduce assertion that reflects the initial one
ChristophHi Apr 17, 2024
e7d927b
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi Apr 18, 2024
cdaf570
Introduce toggle and base addedToCart on success event only
ChristophHi Apr 18, 2024
3b68cea
Fix pickupStore name
ChristophHi Apr 18, 2024
d8edb1b
Add JSDoc
ChristophHi Apr 18, 2024
029265e
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi Apr 18, 2024
832b0f8
Resolve conflicts
ChristophHi Apr 30, 2024
6cf65cc
Review feedback: Let multiCartService determine existing entries
ChristophHi Apr 30, 2024
29a928a
Revert unintended formattingchanges
ChristophHi Apr 30, 2024
923d029
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi Apr 30, 2024
227b42e
Tell merge state from OCC response
ChristophHi Apr 30, 2024
654de28
Merge branch 'fix/CXSPA-6791-addToCart' of https://github.com/SAP/spa…
ChristophHi Apr 30, 2024
6c69495
Remove non-intended files
ChristophHi Apr 30, 2024
c06ee32
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi Apr 30, 2024
7a40936
Improve JS doc, added further deprecations
ChristophHi May 2, 2024
0b2aa9e
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi May 2, 2024
687a8fa
Improve test descriptions
ChristophHi May 2, 2024
ee04c39
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi May 6, 2024
f07a8e2
fix sonar issue
ChristophHi May 6, 2024
df081b5
Trigger Build
ChristophHi May 6, 2024
fbd7719
Review feedback II + take pickup name from response
ChristophHi May 7, 2024
a9e599f
Merge branch 'develop' into fix/CXSPA-6791-addToCart
ChristophHi May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { ElementRef, ViewContainerRef } from '@angular/core';
import { AbstractType, ElementRef, ViewContainerRef } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
CartAddEntryFailEvent,
CartAddEntrySuccessEvent,
CartUiEventAddToCart,
} from '@spartacus/cart/base/root';
import { CxEvent, EventService } from '@spartacus/core';
import { LaunchDialogService, LAUNCH_CALLER } from '@spartacus/storefront';
import { CxEvent, EventService, FeatureConfigService } from '@spartacus/core';
import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront';
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
import { AddedToCartDialogEventListener } from './added-to-cart-dialog-event.listener';

const mockEventStream$ = new BehaviorSubject<CxEvent>({});
const mockEventSuccessStream$ = new BehaviorSubject<CxEvent>({});

class MockEventService implements Partial<EventService> {
get(): Observable<any> {
return mockEventStream$.asObservable();
get<T>(eventType: AbstractType<T>): Observable<T> {
if (
eventType.name === CartUiEventAddToCart.type ||
eventType.name === CartAddEntryFailEvent.type
) {
return mockEventStream$.asObservable() as Observable<T>;
} else {
return mockEventSuccessStream$.asObservable() as Observable<T>;
}
}
}

Expand All @@ -29,6 +38,7 @@ class MockLaunchDialogService implements Partial<LaunchDialogService> {
}

const mockEvent = new CartUiEventAddToCart();
const mockSuccessEvent = new CartAddEntrySuccessEvent();
mockEvent.productCode = 'test';
mockEvent.quantity = 3;
mockEvent.numberOfEntriesBeforeAdd = 1;
Expand All @@ -37,9 +47,10 @@ mockEvent.pickupStoreName = 'testStore';
const mockFailEvent = new CartAddEntryFailEvent();
mockFailEvent.error = {};

describe('AddToCartDialogEventListener', () => {
describe('AddedToCartDialogEventListener', () => {
let listener: AddedToCartDialogEventListener;
let launchDialogService: LaunchDialogService;
let featureConfigService: FeatureConfigService;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -56,34 +67,66 @@ describe('AddToCartDialogEventListener', () => {
],
});

listener = TestBed.inject(AddedToCartDialogEventListener);
featureConfigService = TestBed.inject(FeatureConfigService);
launchDialogService = TestBed.inject(LaunchDialogService);
});

describe('onAddToCart', () => {
it('Should open modal on event', () => {
it('should open modal on event CartAddEntrySuccessEvent in case the toggle is active', () => {
spyOn(featureConfigService, 'isEnabled').and.returnValue(true);
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(listener as any, 'openModalAfterSuccess').and.stub();
mockEventSuccessStream$.next(mockSuccessEvent);
expect(listener['openModalAfterSuccess']).toHaveBeenCalledWith(
mockSuccessEvent
);
});

it('should not open modal on event CartAddEntrySuccessEvent in case the toggle is inactive', () => {
spyOn(featureConfigService, 'isEnabled').and.returnValue(false);
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(listener as any, 'openModalAfterSuccess').and.stub();
mockEventSuccessStream$.next(mockSuccessEvent);
expect(listener['openModalAfterSuccess']).not.toHaveBeenCalled();
});

it('should open modal on event CartUiEventAddToCart in case the toggle is inactive', () => {
spyOn(featureConfigService, 'isEnabled').and.returnValue(false);
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(listener as any, 'openModal').and.stub();
mockEventStream$.next(mockEvent);
expect(listener['openModal']).toHaveBeenCalledWith(mockEvent);
});

it('Should close modal on fail event', () => {
it('should not open modal on event CartUiEventAddToCart in case the toggle is active', () => {
spyOn(featureConfigService, 'isEnabled').and.returnValue(true);
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(listener as any, 'openModal').and.stub();
mockEventStream$.next(mockEvent);
expect(listener['openModal']).not.toHaveBeenCalled();
});

it('should close modal on fail event in case toggle is inactive', () => {
spyOn(featureConfigService, 'isEnabled').and.returnValue(false);
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(listener as any, 'closeModal').and.stub();
mockEventStream$.next(mockFailEvent);
expect(listener['closeModal']).toHaveBeenCalledWith(mockFailEvent);
});
});

describe('openModal', () => {
it('Should open the add to cart dialog', () => {
it('should open the add to cart dialog', () => {
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(launchDialogService, 'openDialog').and.callThrough();
listener['openModal'](mockEvent);
expect(launchDialogService.openDialog).toHaveBeenCalled();
});
});

describe('closeModal', () => {
it('Should close the add to cart dialog', () => {
it('should close the add to cart dialog', () => {
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(launchDialogService, 'closeDialog').and.stub();
listener['closeModal']('reason');
expect(launchDialogService.closeDialog).toHaveBeenCalledWith('reason');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Injectable, OnDestroy } from '@angular/core';
import { Injectable, OnDestroy, inject } from '@angular/core';
import {
CartAddEntryFailEvent,
CartAddEntrySuccessEvent,
CartUiEventAddToCart,
} from '@spartacus/cart/base/root';
import { EventService } from '@spartacus/core';
import { LaunchDialogService, LAUNCH_CALLER } from '@spartacus/storefront';
import { EventService, FeatureConfigService } from '@spartacus/core';
import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';

Expand All @@ -19,6 +20,7 @@ import { take } from 'rxjs/operators';
})
export class AddedToCartDialogEventListener implements OnDestroy {
protected subscription = new Subscription();
private featureConfig = inject(FeatureConfigService);

constructor(
protected eventService: EventService,
Expand All @@ -28,19 +30,33 @@ export class AddedToCartDialogEventListener implements OnDestroy {
}

protected onAddToCart() {
this.subscription.add(
this.eventService.get(CartUiEventAddToCart).subscribe((event) => {
this.openModal(event);
})
);

this.subscription.add(
this.eventService.get(CartAddEntryFailEvent).subscribe((event) => {
this.closeModal(event);
})
);
if (
this.featureConfig.isEnabled('adddedToCartDialogDrivenBySuccessEvent')
) {
this.subscription.add(
this.eventService
.get(CartAddEntrySuccessEvent)
.subscribe((successEvent) => {
this.openModalAfterSuccess(successEvent);
})
);
} else {
this.subscription.add(
this.eventService.get(CartUiEventAddToCart).subscribe((event) => {
this.openModal(event);
})
);
this.subscription.add(
this.eventService.get(CartAddEntryFailEvent).subscribe((event) => {
this.closeModal(event);
})
);
}
}

/**
* Opens modal based on CartUiEventAddToCart.
* @param event Signals that a product has been added to the cart.
*/
ChristophHi marked this conversation as resolved.
Show resolved Hide resolved
protected openModal(event: CartUiEventAddToCart): void {
const addToCartData = {
productCode: event.productCode,
Expand All @@ -61,6 +77,32 @@ export class AddedToCartDialogEventListener implements OnDestroy {
}
}

/**
* Opens modal after addToCart has been successfully performed.
* @param successEvent Signals that the backend call succeeded.
*/
protected openModalAfterSuccess(
successEvent: CartAddEntrySuccessEvent
): void {
const addToCartData = {
productCode: successEvent.productCode,
quantity: successEvent.quantity,
numberOfEntriesBeforeAdd: successEvent.numberOfEntriesBeforeAdd,
pickupStoreName: successEvent.pickupStore,
};

const dialog = this.launchDialogService.openDialog(
LAUNCH_CALLER.ADDED_TO_CART,
undefined,
undefined,
addToCartData
);

if (dialog) {
dialog.pipe(take(1)).subscribe();
}
}

protected closeModal(reason?: any): void {
this.launchDialogService.closeDialog(reason);
}
Expand Down
15 changes: 9 additions & 6 deletions feature-libs/cart/base/core/facade/active-cart.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { Cart, MultiCartFacade, OrderEntry } from '@spartacus/cart/base/root';
import {
getLastValueSync,
OCC_CART_ID_CURRENT,
OCC_USER_ID_ANONYMOUS,
OCC_USER_ID_CURRENT,
OCC_USER_ID_GUEST,
StateUtils,
UserIdService,
WindowRef,
getLastValueSync,
} from '@spartacus/core';
import { BehaviorSubject, EMPTY, Observable, of, Subject } from 'rxjs';
import { BehaviorSubject, EMPTY, Observable, Subject, of } from 'rxjs';
import { take } from 'rxjs/operators';
import { ActiveCartService } from './active-cart.service';

Expand Down Expand Up @@ -457,9 +457,10 @@ describe('ActiveCartService', () => {
});

describe('addEntry', () => {
const entries = [{}, {}, {}];
it('should just add entry after cart is provided', () => {
spyOn<any>(service, 'requireLoadedCart').and.returnValue(
of({ code: 'code', guid: 'guid' })
of({ code: 'code', guid: 'guid', entries: entries })
);
spyOn(multiCartFacade, 'addEntry').and.callThrough();
userId$.next(OCC_USER_ID_ANONYMOUS);
Expand All @@ -471,13 +472,14 @@ describe('ActiveCartService', () => {
'guid',
'productCode',
2,
undefined
undefined,
3
);
});

it('should handle pickup in store', () => {
spyOn<any>(service, 'requireLoadedCart').and.returnValue(
of({ code: 'code', guid: 'guid' })
of({ code: 'code', guid: 'guid', entries: entries })
);
spyOn(multiCartFacade, 'addEntry').and.callThrough();
userId$.next(OCC_USER_ID_ANONYMOUS);
Expand All @@ -489,7 +491,8 @@ describe('ActiveCartService', () => {
'guid',
'productCode',
2,
'pickupStore'
'pickupStore',
3
);
});
});
Expand Down
3 changes: 2 additions & 1 deletion feature-libs/cart/base/core/facade/active-cart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,8 @@ export class ActiveCartService implements ActiveCartFacade, OnDestroy {
getCartIdByUserId(cart, userId),
productCode,
quantity,
pickupStore
pickupStore,
cart.entries?.length
);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,12 +401,13 @@ describe('MultiCartService', () => {
productCode: 'productCode',
quantity: 2,
pickupStore: undefined,
numberOfEntriesBeforeAdd: undefined,
})
);
});

it('should dispatch addEntry action with pickupStore', () => {
service.addEntry('userId', 'cartId', 'productCode', 2, 'pickupStore');
service.addEntry('userId', 'cartId', 'productCode', 2, 'pickupStore', 3);

expect(store.dispatch).toHaveBeenCalledWith(
new CartActions.CartAddEntry({
Expand All @@ -415,6 +416,7 @@ describe('MultiCartService', () => {
productCode: 'productCode',
quantity: 2,
pickupStore: 'pickupStore',
numberOfEntriesBeforeAdd: 3,
})
);
});
Expand Down
4 changes: 3 additions & 1 deletion feature-libs/cart/base/core/facade/multi-cart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ export class MultiCartService implements MultiCartFacade {
cartId: string,
productCode: string,
quantity: number,
pickupStore?: string
pickupStore?: string,
numberOfEntriesBeforeAdd?: number
ChristophHi marked this conversation as resolved.
Show resolved Hide resolved
): void {
this.store.dispatch(
new CartActions.CartAddEntry({
Expand All @@ -236,6 +237,7 @@ export class MultiCartService implements MultiCartFacade {
productCode,
quantity,
pickupStore,
numberOfEntriesBeforeAdd,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class CartAddEntry extends StateUtils.EntityProcessesIncrementAction {
productCode: string;
quantity: number;
pickupStore?: string;
numberOfEntriesBeforeAdd?: number;
}
) {
super(MULTI_CART_DATA, payload.cartId);
Expand All @@ -48,6 +49,7 @@ export class CartAddEntrySuccess extends StateUtils.EntityProcessesDecrementActi
quantityAdded?: number;
statusCode?: string;
statusMessage?: string;
numberOfEntriesBeforeAdd?: number;
}
) {
super(MULTI_CART_DATA, payload.cartId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { CartActions } from '../actions/index';
import * as fromEffects from './cart-entry.effect';
import createSpy = jasmine.createSpy;

const replacedProductCode = 'replacedProduct';

const MockOccModuleConfig: OccConfig = {
backend: {
occ: {
Expand All @@ -31,7 +33,7 @@ describe('Cart effect', () => {
beforeEach(() => {
mockCartModification = {
deliveryModeChanged: true,
entry: {},
entry: { product: { code: replacedProductCode } },
quantity: 1,
quantityAdded: 1,
statusCode: 'statusCode',
Expand Down Expand Up @@ -68,7 +70,7 @@ describe('Cart effect', () => {
const completion = new CartActions.CartAddEntrySuccess({
userId,
cartId,
productCode: 'testProductCode',
productCode: replacedProductCode,
...mockCartModification,
});

Expand All @@ -89,7 +91,7 @@ describe('Cart effect', () => {
const completion = new CartActions.CartAddEntrySuccess({
userId,
cartId,
productCode: 'testProductCode',
productCode: replacedProductCode,
pickupStore: 'pickupStore',
...mockCartModification,
});
Expand Down