Skip to content

Commit

Permalink
feat: quote item list shall be editable when quote is switched to edi…
Browse files Browse the repository at this point in the history
…t mode [CXSPA-4231] (#17731)
  • Loading branch information
Uli-Tiger authored Aug 7, 2023
1 parent ff507a1 commit 44b4007
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -422,5 +422,30 @@ describe('CartItemListComponent', () => {
expect(component.readonly).toEqual(mockContext.readonly);
expect(setLoading).toHaveBeenCalledWith(mockContext.cartIsLoading);
});

it('should mark view for check and force re-creation of item controls when outlet context emits with changed read-only flag', () => {
const secondMockContext = structuredClone(mockContext);
secondMockContext.readonly = false;
const context$ = of(mockContext, secondMockContext);
configureTestingModule().overrideProvider(OutletContextData, {
useValue: { context$ },
});
TestBed.compileComponents();
stubSeviceAndCreateComponent();
const control0 = component.form.get(mockItem0.entryNumber.toString());
const control1 = component.form.get(mockItem1.entryNumber.toString());
spyOn(component['cd'], 'markForCheck').and.callThrough();

component.ngOnInit();

expect(component['cd'].markForCheck).toHaveBeenCalled();
expect(control0).not.toBe(
component.form.get(mockItem0.entryNumber.toString())
);
expect(control1).not.toBe(
component.form.get(mockItem1.entryNumber.toString())
);
expect(component['_forceReRender']).toBe(false); // flag reset
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class CartItemListComponent implements OnInit, OnDestroy {
@Input() cartId: string;

protected _items: OrderEntry[] = [];
protected _forceReRender: boolean = false;
form: UntypedFormGroup = new UntypedFormGroup({});

@Input('items')
Expand Down Expand Up @@ -107,7 +108,9 @@ export class CartItemListComponent implements OnInit, OnDestroy {
protected getInputsFromContext(): Subscription | undefined {
return this.outlet?.context$.subscribe((context) => {
if (context.readonly !== undefined) {
this._forceReRender = this.readonly !== context.readonly;
this.readonly = context.readonly;
this.cd.markForCheck();
}
if (context.hasHeader !== undefined) {
this.hasHeader = context.hasHeader;
Expand Down Expand Up @@ -173,6 +176,7 @@ export class CartItemListComponent implements OnInit, OnDestroy {
) {
const index = i - offset;
if (
this._forceReRender ||
JSON.stringify(this._items?.[index]) !== JSON.stringify(items[index])
) {
if (this._items[index]) {
Expand All @@ -186,6 +190,7 @@ export class CartItemListComponent implements OnInit, OnDestroy {
}
}
}
this._forceReRender = false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<ng-template
[cxOutlet]="cartOutlets.CART_ITEM_LIST"
[cxOutletContext]="{
items: quoteDetails.entries,
readonly: true
items: quoteDetails.entries ?? [],
readonly: !quoteDetails.isEditable
}"
>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,93 @@
import { TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import {
Quote,
QuoteDetailsReloadQueryEvent,
QuoteFacade,
} from '@spartacus/quote/root';
import { QuoteDetailsCartComponent } from './quote-details-cart.component';
import { Quote, QuoteFacade } from '@spartacus/quote/root';

import { I18nTestingModule } from '@spartacus/core';
import { IconTestingModule } from '@spartacus/storefront';
import { Observable, of } from 'rxjs';
import { Directive, Input } from '@angular/core';
import { By } from '@angular/platform-browser';
import {
CartRemoveEntrySuccessEvent,
CartUpdateEntrySuccessEvent,
} from '@spartacus/cart/base/root';
import { EventService, I18nTestingModule } from '@spartacus/core';
import { IconTestingModule, OutletDirective } from '@spartacus/storefront';
import { EMPTY, Observable, Subject, of } from 'rxjs';
import {
QUOTE_CODE,
createEmptyQuote,
} from '../../../core/testing/quote-test-utils';
import { By } from '@angular/platform-browser';

const quote: Quote = createEmptyQuote();
@Directive({
selector: '[cxOutlet]',
})
class MockOutletDirective implements Partial<OutletDirective> {
@Input() cxOutlet: string;
@Input() cxOutletContext: string;
}

class MockQuoteFacade implements Partial<QuoteFacade> {
getQuoteDetails(): Observable<Quote> {
return of(quote);
return of(createEmptyQuote());
}
}

describe('QuoteDetailsCartComponent', () => {
let mockedEventService: EventService;
let fixture: ComponentFixture<QuoteDetailsCartComponent>;
let component: QuoteDetailsCartComponent;

beforeEach(
waitForAsync(() => {
initMocks();
TestBed.configureTestingModule({
imports: [I18nTestingModule, IconTestingModule],
declarations: [QuoteDetailsCartComponent, MockOutletDirective],
providers: [
{
provide: QuoteFacade,
useClass: MockQuoteFacade,
},
{
provide: EventService,
useValue: mockedEventService,
},
],
}).compileComponents();
})
);

beforeEach(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule, IconTestingModule],
declarations: [QuoteDetailsCartComponent],
providers: [
{
provide: QuoteFacade,
useClass: MockQuoteFacade,
},
],
});
fixture = TestBed.createComponent(QuoteDetailsCartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

function initMocks() {
mockedEventService = jasmine.createSpyObj('eventService', [
'get',
'dispatch',
]);
asSpy(mockedEventService.get).and.returnValue(EMPTY);
}

function asSpy(f: any) {
return <jasmine.Spy>f;
}

it('should create the component', () => {
const fixture = TestBed.createComponent(QuoteDetailsCartComponent);
const component = fixture.componentInstance;
expect(component).toBeTruthy();
});

it('should per default display CARET_UP', () => {
const fixture = TestBed.createComponent(QuoteDetailsCartComponent);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.textContent).toContain(
'CARET_UP'
);
});

it('should toggle caret when clicked', () => {
const fixture = TestBed.createComponent(QuoteDetailsCartComponent);
fixture.detectChanges();
const caret = fixture.debugElement.query(
By.css('.cart-toggle')
).nativeElement;
Expand All @@ -61,11 +99,28 @@ describe('QuoteDetailsCartComponent', () => {
});

it('should provide quote details observable', (done) => {
const fixture = TestBed.createComponent(QuoteDetailsCartComponent);
const component = fixture.componentInstance;
component.quoteDetails$.subscribe((quoteDetails) => {
expect(quoteDetails.code).toBe(QUOTE_CODE);
done();
});
});

it('should dispatch quote reload event when cart changes', () => {
asSpy(mockedEventService.get).and.returnValue(
of(new CartUpdateEntrySuccessEvent(), new CartRemoveEntrySuccessEvent())
);
component.ngOnInit();
expect(mockedEventService.dispatch).toHaveBeenCalledTimes(2);
expect(mockedEventService.dispatch).toHaveBeenCalledWith(
{},
QuoteDetailsReloadQueryEvent
);
});

it('should close subscriptions on destroy', () => {
asSpy(mockedEventService.get).and.returnValue(new Subject());
component.ngOnInit();
component.ngOnDestroy();
expect(component['subscription'].closed).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,46 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Component } from '@angular/core';
import { CartOutlets } from '@spartacus/cart/base/root';
import { Quote, QuoteFacade } from '@spartacus/quote/root';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CartEvent, CartOutlets } from '@spartacus/cart/base/root';
import { EventService } from '@spartacus/core';
import {
Quote,
QuoteDetailsReloadQueryEvent,
QuoteFacade,
} from '@spartacus/quote/root';
import { ICON_TYPE } from '@spartacus/storefront';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { QuoteDetailsCartComponentService } from './quote-details-cart.component.service';

@Component({
selector: 'cx-quote-details-cart',
templateUrl: './quote-details-cart.component.html',
})
export class QuoteDetailsCartComponent {
export class QuoteDetailsCartComponent implements OnInit, OnDestroy {
quoteDetails$: Observable<Quote> = this.quoteFacade.getQuoteDetails();
iconTypes = ICON_TYPE;
readonly cartOutlets = CartOutlets;
showCart$ = this.quoteDetailsCartService.getQuoteEntriesExpanded();
protected subscription: Subscription;

constructor(
protected quoteFacade: QuoteFacade,
protected quoteDetailsCartService: QuoteDetailsCartComponentService
protected quoteDetailsCartService: QuoteDetailsCartComponentService,
protected eventService: EventService
) {}

ngOnInit(): void {
// if anything in the cart changes, reload the quote
this.subscription = this.eventService.get(CartEvent).subscribe(() => {
this.eventService.dispatch({}, QuoteDetailsReloadQueryEvent);
});
}

ngOnDestroy(): void {
this.subscription.unsubscribe();
}

onToggleShowOrHideCart(showCart: boolean) {
this.quoteDetailsCartService.setQuoteEntriesExpanded(!showCart);
}
Expand Down

0 comments on commit 44b4007

Please sign in to comment.