Skip to content

Commit

Permalink
CXSPA-3856 quote details: Status, prices, entries, refresh issues (#1…
Browse files Browse the repository at this point in the history
…7596)

Co-authored-by: Moritz Schaefer <[email protected]>
  • Loading branch information
ChristophHi and Stofftiger authored Jul 11, 2023
1 parent aea911a commit 36a3c06
Show file tree
Hide file tree
Showing 22 changed files with 388 additions and 198 deletions.
1 change: 1 addition & 0 deletions feature-libs/quote/assets/translations/en/quote.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const quote = {
created: 'Created',
lastUpdated: 'Last Updated',
estimatedTotal: 'Estimated Total',
total: 'Total',
description: 'Description',
expiryDate: 'Expiry Date',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<span class="cart-toggle-text">{{ 'quote.commons.cart' | cxTranslate }}</span>
</div>

<ng-container *ngIf="showCart && (quoteDetails$ | async) as quoteDetailsState">
<ng-container *ngIf="showCart && (quoteDetails$ | async) as quoteDetails">
<ng-template
[cxOutlet]="cartOutlets.CART_ITEM_LIST"
[cxOutletContext]="{
items: quoteDetailsState.data?.entries,
items: quoteDetails.entries,
readonly: true
}"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { QuoteDetailsCartComponent } from './quote-details-cart.component';
import { QuoteFacade } from '@spartacus/quote/root';
import { MockQuoteFacade } from '../overview/quote-details-overview.component.spec';
import { Quote, QuoteFacade } from '@spartacus/quote/root';

import { I18nTestingModule } from '@spartacus/core';
import { IconTestingModule } from '@spartacus/storefront';
import { Observable, of } from 'rxjs';
import {
QUOTE_CODE,
createEmptyQuote,
} from '../../../core/testing/quote-test-utils';
import { By } from '@angular/platform-browser';

const quote: Quote = createEmptyQuote();

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

describe('QuoteDetailsCartComponent', () => {
beforeEach(() => {
Expand All @@ -24,4 +38,34 @@ describe('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;
caret.click();
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.textContent).toContain(
'CARET_DOWN'
);
});

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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@

import { Component } from '@angular/core';
import { CartOutlets } from '@spartacus/cart/base/root';
import { QuoteFacade } from '@spartacus/quote/root';
import { Quote, QuoteFacade } from '@spartacus/quote/root';
import { ICON_TYPE } from '@spartacus/storefront';
import { Observable } from 'rxjs';

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

constructor(protected quoteFacade: QuoteFacade) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { IconModule, OutletModule } from '@spartacus/storefront';
import { QuoteDetailsCartComponent } from './quote-details-cart.component';
import { QuoteDetailsCartSummaryComponent } from './summary/quote-details-cart-summary.component';

//https://jira.tools.sap/browse/CXSPA-4039

//CartBaseComponentsModule import in order to ensure that the cart outlet implementations are
//loaded once this component is displayed. Still after one interaction, outlet displays twice

//Side note: importing CartBaseModule will lead to a duplicate rendering of the cart item list outlet
@NgModule({
imports: [CommonModule, OutletModule, IconModule, I18nModule],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { QuoteFacade } from '@spartacus/quote/root';
templateUrl: 'quote-details-cart-summary.component.html',
})
export class QuoteDetailsCartSummaryComponent {
quoteDetails$ = this.quoteFacade.getQuoteDetails();
quoteDetails$ = this.quoteFacade.getQuoteDetailsQueryState();

readonly cartOutlets = CartOutlets;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,84 @@
<ng-container *ngIf="quoteDetails$ | async as quoteDetailsState">
<ng-container *ngIf="!quoteDetailsState.loading; else loading">
<div class="cx-quote-details-header-status">
<h3 class="quote-details-header">
{{ 'quote.commons.id' | cxTranslate }}:
{{ quoteDetailsState?.data?.code }}
</h3>
<h3 class="status">
{{ 'quote.commons.status' | cxTranslate }}
{{ 'quote.states.' + quoteDetailsState?.data?.state | cxTranslate }}
</h3>
</div>
<div class="cx-quote-overview">
<div class="container">
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(
quoteDetailsState?.data?.code || 'TODO CHHI',
'quote.details.code'
) | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetailsState?.data?.creationTime | cxDate,
'quote.details.created'
) | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetailsState?.data?.updatedTime | cxDate,
'quote.details.lastUpdated'
) | async
"
></cx-card>
</div>
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(
quoteDetailsState?.data?.previousEstimatedTotal
?.formattedValue || 'TODO CHHI',
'quote.details.estimatedTotal'
) | async
"
></cx-card>
<cx-card
class="cx-card-description"
[content]="
getCardContent(
quoteDetailsState?.data?.description || 'TODO CHHI',
'quote.details.description'
) | async
"
[truncateText]="true"
[charactersLimit]="30"
></cx-card>
</div>
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(
'quote.states.' + quoteDetailsState?.data?.state | cxTranslate,
'quote.commons.status'
) | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetailsState?.data?.expirationTime | cxDate,
'quote.details.expiryDate'
) | async
"
></cx-card>
</div>
<ng-container *ngIf="quoteDetails$ | async as quoteDetails; else loading">
<div class="cx-quote-details-header-status">
<h3 class="quote-details-header">
{{ 'quote.commons.id' | cxTranslate }}:
{{ quoteDetails.code }}
</h3>
<h3 class="status">
{{ 'quote.commons.status' | cxTranslate }}
{{ 'quote.states.' + quoteDetails.state | cxTranslate }}
</h3>
</div>
<div class="cx-quote-overview">
<div class="container">
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(quoteDetails.code, 'quote.details.code') | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetails.creationTime | cxDate,
'quote.details.created'
) | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetails.updatedTime | cxDate,
'quote.details.lastUpdated'
) | async
"
></cx-card>
</div>
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(
getTotalPrice(quoteDetails),
getTotalPriceDescription(quoteDetails)
) | async
"
></cx-card>
<cx-card
class="cx-card-description"
[content]="
getCardContent(
quoteDetails.description,
'quote.details.description'
) | async
"
[truncateText]="true"
[charactersLimit]="30"
></cx-card>
</div>
<div class="cx-summary-card">
<cx-card
[content]="
getCardContent(
'quote.states.' + quoteDetails.state | cxTranslate,
'quote.commons.status'
) | async
"
></cx-card>
<cx-card
[content]="
getCardContent(
quoteDetails.expirationTime | cxDate,
'quote.details.expiryDate'
) | async
"
></cx-card>
</div>
</div>
</ng-container>

<ng-template #loading>
<div class="cx-spinner">
<cx-spinner></cx-spinner>
</div>
</ng-template>
</div>
</ng-container>

<ng-template #loading>
<div class="cx-spinner">
<cx-spinner></cx-spinner>
</div>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ import {
QuoteActionType,
QuoteState,
} from '@spartacus/quote/root';
import {
I18nTestingModule,
QueryState,
TranslationService,
} from '@spartacus/core';
import { I18nTestingModule, TranslationService } from '@spartacus/core';
import { CardModule } from '@spartacus/storefront';

import { Observable, of } from 'rxjs';
import { QuoteDetailsOverviewComponent } from './quote-details-overview.component';
import createSpy = jasmine.createSpy;

const totalPriceFormattedValue = '$20';

const mockCartId = '1234';
const mockAction = { type: QuoteActionType.CREATE, isPrimary: true };
const mockQuote: Quote = {
Expand All @@ -32,17 +30,17 @@ const mockQuote: Quote = {
updatedTime: new Date('2022-06-09T13:31:36+0000'),
previousEstimatedTotal: {
currencyIso: 'USD',
formattedValue: '$0.00',
value: 0,
formattedValue: '$1.00',
value: 1,
},
state: QuoteState.BUYER_ORDERED,
name: 'Name',
totalPrice: { value: 20 },
totalPrice: { value: 20, formattedValue: totalPriceFormattedValue },
};

export class MockQuoteFacade implements Partial<QuoteFacade> {
getQuoteDetails(): Observable<QueryState<Quote>> {
return of({ data: mockQuote, loading: false, error: false });
getQuoteDetails(): Observable<Quote> {
return of(mockQuote);
}
setSort = createSpy();
setCurrentPage = createSpy();
Expand Down Expand Up @@ -143,4 +141,36 @@ describe('QuoteDetailsOverviewComponent', () => {
expect(result).toEqual(expected);
});
});

describe('getTotalPrice', () => {
it('should return the total price formatted value in case it is available', () => {
expect(component.getTotalPrice(mockQuote)).toBe(totalPriceFormattedValue);
});

it('should return null in case no formatted value is available', () => {
const quoteWOPrices: Quote = {
...mockQuote,
totalPrice: {},
};
expect(component.getTotalPrice(quoteWOPrices)).toBe(null);
});
});

describe('getTotalPriceDescription', () => {
it('should name total price as estimated as long as final status not reached', () => {
expect(component.getTotalPriceDescription(mockQuote)).toBe(
'quote.details.estimatedTotal'
);
});

it('should name total price as total as in case final status reached, i.e. checkout action is available', () => {
const quoteInOfferState: Quote = {
...mockQuote,
allowedActions: [{ type: QuoteActionType.CHECKOUT, isPrimary: true }],
};
expect(component.getTotalPriceDescription(quoteInOfferState)).toBe(
'quote.details.total'
);
});
});
});
Loading

0 comments on commit 36a3c06

Please sign in to comment.