Skip to content

Commit

Permalink
fix: addedToCartDialog driven by success event, takes actual product …
Browse files Browse the repository at this point in the history
…code into account (#18731)
  • Loading branch information
ChristophHi committed May 7, 2024
1 parent 4a9fe91 commit 541f7d3
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
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,
PointOfService,
} 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';
import { OrderEntry } from '../../root/models';

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 @@ -28,18 +43,32 @@ class MockLaunchDialogService implements Partial<LaunchDialogService> {
closeDialog(_reason: string): void {}
}

const PRODUCT_CODE = 'productCode';
const STORE_NAME = 'storeName';
const STORE_NAME_FROM_POS = 'storeNameFromPoS';
const QUANTITY = 3;
const deliveryPointOfService: PointOfService = { name: STORE_NAME_FROM_POS };
const entry: OrderEntry = {
quantity: 0,
};

const mockEvent = new CartUiEventAddToCart();
mockEvent.productCode = 'test';
mockEvent.quantity = 3;
const mockSuccessEvent = new CartAddEntrySuccessEvent();
mockEvent.productCode = PRODUCT_CODE;
mockEvent.quantity = QUANTITY;
mockEvent.numberOfEntriesBeforeAdd = 1;
mockEvent.pickupStoreName = 'testStore';
mockEvent.pickupStoreName = STORE_NAME;
mockSuccessEvent.productCode = PRODUCT_CODE;
mockSuccessEvent.quantity = QUANTITY;
mockSuccessEvent.entry = entry;

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,37 +85,133 @@ describe('AddToCartDialogEventListener', () => {
],
});

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

describe('onAddToCart', () => {
it('Should open modal on event', () => {
it('should open modal on event CartAddEntrySuccessEvent in case toggle adddedToCartDialogDrivenBySuccessEvent 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 toggle adddedToCartDialogDrivenBySuccessEvent 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 toggle adddedToCartDialogDrivenBySuccessEvent 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 toggle adddedToCartDialogDrivenBySuccessEvent 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('openModalAfterSuccess', () => {
beforeEach(() => {
listener = TestBed.inject(AddedToCartDialogEventListener);
spyOn(launchDialogService, 'openDialog').and.callThrough();
});

it('should retrieve pickup store name from point of service of new entry added to the cart', () => {
listener['openModalAfterSuccess'](mockSuccessEvent);
expect(launchDialogService.openDialog).toHaveBeenCalledWith(
LAUNCH_CALLER.ADDED_TO_CART,
undefined,
undefined,
{
productCode: PRODUCT_CODE,
quantity: QUANTITY,
pickupStoreName: STORE_NAME_FROM_POS,
addedEntryWasMerged: true,
}
);
});

it('should forward pickup store name as undefined in case no point of service provided in success event', () => {
entry.deliveryPointOfService = undefined;
listener['openModalAfterSuccess'](mockSuccessEvent);
expect(launchDialogService.openDialog).toHaveBeenCalledWith(
LAUNCH_CALLER.ADDED_TO_CART,
undefined,
undefined,
{
productCode: PRODUCT_CODE,
quantity: QUANTITY,
pickupStoreName: undefined,
addedEntryWasMerged: true,
}
);
});
});

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');
});
});

describe('calculateEntryWasMerged', () => {
it('should return true in case no quantityAdded is present (which happens if no stock is available)', () => {
mockSuccessEvent.quantityAdded = undefined;
expect(listener['calculateEntryWasMerged'](mockSuccessEvent)).toBe(true);
});

it('should return true in case the resulting entries quantity exceeds the quantity that was added to the cart', () => {
mockSuccessEvent.quantityAdded = 1;
entry.quantity = 2;
expect(listener['calculateEntryWasMerged'](mockSuccessEvent)).toBe(true);
});

it('should return false in case the resulting entries quantity equals the quantity that was added to the cart', () => {
mockSuccessEvent.quantityAdded = 3;
entry.quantity = 3;
expect(listener['calculateEntryWasMerged'](mockSuccessEvent)).toBe(false);
});

it('should return false in case the resulting entries quantity is undefined (which can happen only in exceptional situations) ', () => {
mockSuccessEvent.quantityAdded = 1;
entry.quantity = undefined;
expect(listener['calculateEntryWasMerged'](mockSuccessEvent)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@
* 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';
import { AddedToCartDialogComponentData } from './added-to-cart-dialog.component';

@Injectable({
providedIn: 'root',
})
export class AddedToCartDialogEventListener implements OnDestroy {
protected subscription = new Subscription();
private featureConfig = inject(FeatureConfigService);

constructor(
protected eventService: EventService,
Expand All @@ -28,21 +31,39 @@ 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);
})
);
}
}

/**
* @deprecated since 2211.24. With activation of feature toggle 'adddedToCartDialogDrivenBySuccessEvent'
* the method is no longer called, instead method openModalAfterSuccess takes care of launching
* the addedToCart dialogue.
*
* Opens modal based on CartUiEventAddToCart.
* @param event Signals that a product has been added to the cart.
*/
protected openModal(event: CartUiEventAddToCart): void {
const addToCartData = {
const addToCartData: AddedToCartDialogComponentData = {
productCode: event.productCode,
quantity: event.quantity,
numberOfEntriesBeforeAdd: event.numberOfEntriesBeforeAdd,
Expand All @@ -61,6 +82,47 @@ 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: AddedToCartDialogComponentData = {
productCode: successEvent.productCode,
quantity: successEvent.quantity,
pickupStoreName: successEvent.entry?.deliveryPointOfService?.name,
addedEntryWasMerged: this.calculateEntryWasMerged(successEvent),
};

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

if (dialog) {
dialog.pipe(take(1)).subscribe();
}
}
/**
* Calculates whether the previous addToCart resulted into an existing entries quantity being increased (new product was merged) or a new entry added.
* @param successEvent Event that reflects a successfull addToCart. In case the event's quantityAdded is undefined or zero,
* the system could have run into stock issues. Then we return true in order to not break the existing dialog behaviour
* @returns Result of addToCart is a quantity increase (i.e. product was merged)
*/
protected calculateEntryWasMerged(
successEvent: CartAddEntrySuccessEvent
): boolean {
const quantityAdded = successEvent.quantityAdded ?? 0;
return (
quantityAdded === 0 ||
(successEvent.entry?.quantity ?? 0) - quantityAdded > 0
);
}

protected closeModal(reason?: any): void {
this.launchDialogService.closeDialog(reason);
}
Expand Down

0 comments on commit 541f7d3

Please sign in to comment.