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 22 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,29 @@
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';
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 +38,23 @@ class MockLaunchDialogService implements Partial<LaunchDialogService> {
closeDialog(_reason: string): void {}
}

const entry: OrderEntry = { quantity: 0 };

const mockEvent = new CartUiEventAddToCart();
const mockSuccessEvent = new CartAddEntrySuccessEvent();
mockEvent.productCode = 'test';
mockEvent.quantity = 3;
mockEvent.numberOfEntriesBeforeAdd = 1;
mockEvent.pickupStoreName = 'testStore';
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 +71,94 @@ 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 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('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.
*/
ChristophHi marked this conversation as resolved.
Show resolved Hide resolved
protected openModal(event: CartUiEventAddToCart): void {
const addToCartData = {
const addToCartData: AddedToCartDialogComponentData = {
productCode: event.productCode,
quantity: event.quantity,
numberOfEntriesBeforeAdd: event.numberOfEntriesBeforeAdd,
Expand All @@ -61,6 +82,45 @@ 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.pickupStore,
addedEntryWasMerged: this.calculateEntryWasMerged(successEvent),
};

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

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

protected calculateEntryWasMerged(
successEvent: CartAddEntrySuccessEvent
): boolean {
// In case quantityAdded zero, the system could have run into stock issues.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From unit tests and implementation I understood that in case of stock issues the property quantityAdded is undefined (not 0).

So what about updating the comment:

Suggested change
// In case quantityAdded zero, the system could have run into stock issues.
// In case `quantityAdded` is `undefined`, the system could have run into stock issues.

Btw. IMHO we could turn this inline comment in to a fully-fledged JSDoc of this method.

// Then we continue and still launch the dialogue in 'merge' mode in order
// to not break the existing behaviour.
ChristophHi marked this conversation as resolved.
Show resolved Hide resolved
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