diff --git a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.spec.ts b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.spec.ts index 906c2a759c2..098459d0214 100644 --- a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.spec.ts +++ b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.spec.ts @@ -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({}); +const mockEventSuccessStream$ = new BehaviorSubject({}); class MockEventService implements Partial { - get(): Observable { - return mockEventStream$.asObservable(); + get(eventType: AbstractType): Observable { + if ( + eventType.name === CartUiEventAddToCart.type || + eventType.name === CartAddEntryFailEvent.type + ) { + return mockEventStream$.asObservable() as Observable; + } else { + return mockEventSuccessStream$.asObservable() as Observable; + } } } @@ -28,18 +43,32 @@ class MockLaunchDialogService implements Partial { 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({ @@ -56,18 +85,49 @@ 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); @@ -75,18 +135,83 @@ describe('AddToCartDialogEventListener', () => { }); 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); + }); + }); }); diff --git a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.ts b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.ts index 379b2246863..59332f2f8ea 100644 --- a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.ts +++ b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog-event.listener.ts @@ -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, @@ -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, @@ -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); } diff --git a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.spec.ts b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.spec.ts index 661138e8d19..1f2c62be0b2 100644 --- a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.spec.ts +++ b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.spec.ts @@ -21,7 +21,7 @@ import { } from '@spartacus/cart/base/root'; import { ActivatedRouterStateSnapshot, - FeaturesConfig, + FeatureConfigService, I18nTestingModule, RouterState, RoutingService, @@ -62,13 +62,18 @@ class MockActiveCartService implements Partial { } } +const PRODUCT_CODE = 'CODE1111'; +const QUANTITY = 3; +const NUMBER_ENTRIES_BEFORE_ADD = 2; +const PICKUP_STORE_NAME = 'testStore'; +let numberOfEntriesBeforeAdd: number | undefined = NUMBER_ENTRIES_BEFORE_ADD; class MockLaunchDialogService implements Partial { get data$(): Observable { return of({ - productCode: 'CODE1111', - quantity: 3, - numberOfEntriesBeforeAdd: 2, - pickupStoreName: 'test', + productCode: PRODUCT_CODE, + quantity: QUANTITY, + numberOfEntriesBeforeAdd: numberOfEntriesBeforeAdd, + pickupStoreName: PICKUP_STORE_NAME, }); } @@ -80,14 +85,14 @@ const mockOrderEntries: OrderEntry[] = [ quantity: 1, entryNumber: 1, product: { - code: 'CODE1111', + code: PRODUCT_CODE, }, }, { quantity: 2, entryNumber: 1, product: { - code: 'CODE1111', + code: PRODUCT_CODE, }, }, ]; @@ -133,6 +138,7 @@ describe('AddedToCartDialogComponent', () => { let el: DebugElement; let activeCartFacade: ActiveCartFacade; let launchDialogService: LaunchDialogService; + let featureConfigService: FeatureConfigService; beforeEach(() => { TestBed.configureTestingModule({ @@ -160,12 +166,6 @@ describe('AddedToCartDialogComponent', () => { provide: RoutingService, useClass: MockRoutingService, }, - { - provide: FeaturesConfig, - useValue: { - features: { level: '1.3' }, - }, - }, { provide: LaunchDialogService, useClass: MockLaunchDialogService }, ], }).compileComponents(); @@ -178,6 +178,7 @@ describe('AddedToCartDialogComponent', () => { activeCartFacade = TestBed.inject(ActiveCartFacade); launchDialogService = TestBed.inject(LaunchDialogService); + featureConfigService = TestBed.inject(FeatureConfigService); spyOn(activeCartFacade, 'updateEntry').and.callThrough(); @@ -378,4 +379,59 @@ describe('AddedToCartDialogComponent', () => { el.click(); expect(component.dismissModal).toHaveBeenCalledWith('Cross click'); }); + + describe('init()', () => { + it('should compile addedCartEntryWasMerged$ from respective input attribute in case feature toggle adddedToCartDialogDrivenBySuccessEvent is active', () => { + spyOn(featureConfigService, 'isEnabled').and.returnValue(true); + component.init( + PRODUCT_CODE, + QUANTITY, + NUMBER_ENTRIES_BEFORE_ADD, + '', + true + ); + expect(component.addedEntryWasMerged$).toBeObservable( + cold('(t|)', { t: true }) + ); + }); + it('should compile addedCartEntryWasMerged$ handling undefined input in case feature toggle adddedToCartDialogDrivenBySuccessEvent is active', () => { + spyOn(featureConfigService, 'isEnabled').and.returnValue(true); + component.init( + PRODUCT_CODE, + QUANTITY, + NUMBER_ENTRIES_BEFORE_ADD, + '', + undefined + ); + expect(component.addedEntryWasMerged$).toBeObservable( + cold('(t|)', { t: false }) + ); + }); + it('should compile addedCartEntryWasMerged$ from quantity comparison in case feature toggle adddedToCartDialogDrivenBySuccessEvent is not active', () => { + spyOn(featureConfigService, 'isEnabled').and.returnValue(false); + spyOn(activeCartFacade, 'getEntries').and.returnValue( + cold('a', { a: mockOrderEntries }) + ); + component.loaded$ = cold('t', { t: true }); + component.init(PRODUCT_CODE, QUANTITY, NUMBER_ENTRIES_BEFORE_ADD); + expect(component.addedEntryWasMerged$).toBeObservable( + cold('t', { t: true }) + ); + }); + }); + + describe('ngOnInit()', () => { + it('should default numberOfEntriesBeforeAdd with zero in case it is not provided from outside', () => { + numberOfEntriesBeforeAdd = undefined; + spyOn(component, 'init').and.callThrough(); + component.ngOnInit(); + expect(component.init).toHaveBeenCalledWith( + PRODUCT_CODE, + QUANTITY, + 0, + PICKUP_STORE_NAME, + undefined + ); + }); + }); }); diff --git a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.ts b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.ts index f108c5a8143..527e84b4f20 100644 --- a/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.ts +++ b/feature-libs/cart/base/components/added-to-cart-dialog/added-to-cart-dialog.component.ts @@ -11,22 +11,22 @@ import { HostListener, OnDestroy, OnInit, + inject, } from '@angular/core'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { ActiveCartFacade, Cart, - CartUiEventAddToCart, OrderEntry, PromotionLocation, } from '@spartacus/cart/base/root'; -import { RoutingService } from '@spartacus/core'; +import { FeatureConfigService, RoutingService } from '@spartacus/core'; import { FocusConfig, ICON_TYPE, LaunchDialogService, } from '@spartacus/storefront'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, of } from 'rxjs'; import { filter, map, @@ -36,12 +36,29 @@ import { tap, } from 'rxjs/operators'; +export interface AddedToCartDialogComponentData { + productCode: string; + quantity: number; + /** + * Number of cart entries before addToCart was triggered. + * @deprecated since 2211.24. Enable feature toggle 'adddedToCartDialogDrivenBySuccessEvent' + * and use attribute addedEntryWasMerged instead. + */ + numberOfEntriesBeforeAdd?: number; + pickupStoreName?: string; + /** + * Tells whether the product added to the cart was merged into an existing cart entry (with increased quantity), + * or the system created a new cart entry. + */ + addedEntryWasMerged?: boolean; +} @Component({ selector: 'cx-added-to-cart-dialog', templateUrl: './added-to-cart-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddedToCartDialogComponent implements OnInit, OnDestroy { + private featureConfig = inject(FeatureConfigService); iconTypes = ICON_TYPE; entry$: Observable; @@ -83,12 +100,15 @@ export class AddedToCartDialogComponent implements OnInit, OnDestroy { ngOnInit(): void { this.subscription.add( this.launchDialogService.data$.subscribe( - (dialogData: CartUiEventAddToCart) => { + (dialogData: AddedToCartDialogComponentData) => { this.init( dialogData.productCode, dialogData.quantity, - dialogData.numberOfEntriesBeforeAdd, - dialogData.pickupStoreName + //numberOfEntriesBeforeAdd is needed only in case + //'adddedToCartDialogDrivenBySuccessEvent' is not active + dialogData.numberOfEntriesBeforeAdd ?? 0, + dialogData.pickupStoreName, + dialogData.addedEntryWasMerged ); } ) @@ -141,18 +161,35 @@ export class AddedToCartDialogComponent implements OnInit, OnDestroy { productCode: string, quantity: number, numberOfEntriesBeforeAdd: number, - pickupStoreName?: string + pickupStoreName?: string, + addedEntryWasMerged = false ): void { // Display last entry for new product code. This always corresponds to // our new item, independently of whether merging occured or not this.entry$ = this.activeCartFacade.getLastEntry(productCode); this.quantity = quantity; - this.addedEntryWasMerged$ = this.getAddedEntryWasMerged( - numberOfEntriesBeforeAdd - ); + this.pickupStoreName = pickupStoreName; + if ( + this.featureConfig.isEnabled('adddedToCartDialogDrivenBySuccessEvent') + ) { + this.addedEntryWasMerged$ = of(addedEntryWasMerged); + } else { + this.addedEntryWasMerged$ = this.getAddedEntryWasMerged( + numberOfEntriesBeforeAdd + ); + } } - + /** + * Determines if the added entry was merged with an existing one. + * + * @deprecated since 2211.24. With activation of feature toggle 'adddedToCartDialogDrivenBySuccessEvent' + * the method will no longer be called, instead the information whether the entry was merged + * or not will be handed over to this component. + * + * @param numberOfEntriesBeforeAdd Number of entries in cart before addToCart has been performed + * @returns Has entry been merged? + */ protected getAddedEntryWasMerged( numberOfEntriesBeforeAdd: number ): Observable { diff --git a/feature-libs/cart/base/core/store/effects/cart-entry.effect.spec.ts b/feature-libs/cart/base/core/store/effects/cart-entry.effect.spec.ts index 39badc809df..102a4730854 100644 --- a/feature-libs/cart/base/core/store/effects/cart-entry.effect.spec.ts +++ b/feature-libs/cart/base/core/store/effects/cart-entry.effect.spec.ts @@ -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: { @@ -31,7 +33,7 @@ describe('Cart effect', () => { beforeEach(() => { mockCartModification = { deliveryModeChanged: true, - entry: {}, + entry: { product: { code: replacedProductCode } }, quantity: 1, quantityAdded: 1, statusCode: 'statusCode', @@ -68,7 +70,7 @@ describe('Cart effect', () => { const completion = new CartActions.CartAddEntrySuccess({ userId, cartId, - productCode: 'testProductCode', + productCode: replacedProductCode, ...mockCartModification, }); @@ -89,7 +91,7 @@ describe('Cart effect', () => { const completion = new CartActions.CartAddEntrySuccess({ userId, cartId, - productCode: 'testProductCode', + productCode: replacedProductCode, pickupStore: 'pickupStore', ...mockCartModification, }); diff --git a/feature-libs/cart/base/core/store/effects/cart-entry.effect.ts b/feature-libs/cart/base/core/store/effects/cart-entry.effect.ts index 326e67637fb..b00179136e7 100644 --- a/feature-libs/cart/base/core/store/effects/cart-entry.effect.ts +++ b/feature-libs/cart/base/core/store/effects/cart-entry.effect.ts @@ -51,6 +51,8 @@ export class CartEntryEffects { (cartModification: CartModification) => new CartActions.CartAddEntrySuccess({ ...payload, + productCode: cartModification.entry?.product + ?.code as Required, ...(cartModification as Required), }) ), diff --git a/integration-libs/epd-visualization/components/visual-picking/visual-picking-tab/product-list/compact-add-to-cart/compact-add-to-cart.component.spec.ts b/integration-libs/epd-visualization/components/visual-picking/visual-picking-tab/product-list/compact-add-to-cart/compact-add-to-cart.component.spec.ts index 10f57c2ed0d..70cff33ff38 100644 --- a/integration-libs/epd-visualization/components/visual-picking/visual-picking-tab/product-list/compact-add-to-cart/compact-add-to-cart.component.spec.ts +++ b/integration-libs/epd-visualization/components/visual-picking/visual-picking-tab/product-list/compact-add-to-cart/compact-add-to-cart.component.spec.ts @@ -10,15 +10,24 @@ import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { AddedToCartDialogEventListener } from '@spartacus/cart/base/components'; -import { ActiveCartFacade, Cart, OrderEntry } from '@spartacus/cart/base/root'; -import { CmsComponent, I18nTestingModule, Product } from '@spartacus/core'; +import { + ActiveCartFacade, + Cart, + CartUiEventAddToCart, + OrderEntry, +} from '@spartacus/cart/base/root'; +import { + CmsComponent, + EventService, + I18nTestingModule, + Product, +} from '@spartacus/core'; import { CmsComponentData, CurrentProductService, IconModule, - LaunchDialogService, LAUNCH_CALLER, + LaunchDialogService, SpinnerModule, } from '@spartacus/storefront'; import { EMPTY, Observable, of } from 'rxjs'; @@ -82,6 +91,10 @@ class MockLaunchDialogService implements Partial { closeDialog(_reason: string): void {} } +class MockEventService implements Partial { + dispatch(_event: T): void {} +} + @Component({ template: '', selector: 'cx-item-counter', @@ -99,7 +112,7 @@ describe('CompactAddToCartComponent', () => { let service: ActiveCartFacade; let currentProductService: CurrentProductService; let el: DebugElement; - let listener: AddedToCartDialogEventListener; + let eventService: EventService; const mockCartEntry: OrderEntry = { entryNumber: 7 }; @@ -129,7 +142,7 @@ describe('CompactAddToCartComponent', () => { provide: CmsComponentData, useValue: MockCmsComponentData, }, - AddedToCartDialogEventListener, + { provide: EventService, useClass: MockEventService }, ], }).compileComponents(); }) @@ -140,10 +153,9 @@ describe('CompactAddToCartComponent', () => { addToCartComponent = fixture.componentInstance; service = TestBed.inject(ActiveCartFacade); currentProductService = TestBed.inject(CurrentProductService); - listener = TestBed.inject(AddedToCartDialogEventListener); - el = fixture.debugElement; + eventService = TestBed.inject(EventService); - spyOn(listener as any, 'openModal').and.stub(); + el = fixture.debugElement; fixture.detectChanges(); }); @@ -176,12 +188,18 @@ describe('CompactAddToCartComponent', () => { spyOn(service, 'addEntry').and.callThrough(); spyOn(service, 'getEntries').and.returnValue(of([mockCartEntry])); spyOn(service, 'isStable').and.returnValue(of(true)); + spyOn(eventService, 'dispatch').and.callThrough(); addToCartComponent.quantity = 1; + const uiEvent: CartUiEventAddToCart = new CartUiEventAddToCart(); + uiEvent.productCode = productCode; + uiEvent.numberOfEntriesBeforeAdd = 1; + uiEvent.quantity = 1; + uiEvent.pickupStoreName = undefined; addToCartComponent.addToCart(); expect(service.addEntry).toHaveBeenCalledWith(productCode, 1, undefined); - expect(listener['openModal']).toHaveBeenCalledTimes(1); + expect(eventService.dispatch).toHaveBeenCalledWith(uiEvent); }); describe('UI', () => { diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index dbd5f447987..2b6a9098c21 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -43,6 +43,12 @@ export interface FeatureTogglesInterface { */ productConfiguratorAttributeTypesV2?: boolean; + /** + * The addedToCart dialog is driven by 'CartAddEntrySuccessEvent'. Previously it was driven + * by 'CartUiEventAddToCart' event. Code changes affect 'AddedToCartDialogEventListener' + */ + adddedToCartDialogDrivenBySuccessEvent?: boolean; + /** * Adds asterisks to required form fields in all components existing before v2211.20 */ @@ -188,6 +194,7 @@ export const defaultFeatureToggles: Required = { pdfInvoicesSortByInvoiceDate: false, storeFrontLibCardParagraphTruncated: false, productConfiguratorAttributeTypesV2: false, + adddedToCartDialogDrivenBySuccessEvent: false, a11yRequiredAsterisks: false, a11yQuantityOrderTabbing: false, a11yNavigationUiKeyboardControls: false, diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index fa8e926c3f6..ca64aab432e 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -266,6 +266,7 @@ if (environment.requestedDeliveryDate) { pdfInvoicesSortByInvoiceDate: false, storeFrontLibCardParagraphTruncated: true, productConfiguratorAttributeTypesV2: true, + adddedToCartDialogDrivenBySuccessEvent: true, a11yRequiredAsterisks: true, a11yQuantityOrderTabbing: true, a11yNavigationUiKeyboardControls: true,