Skip to content

Commit

Permalink
feature: quote-header-price component - minimal scope (#18003)
Browse files Browse the repository at this point in the history
  • Loading branch information
Uli-Tiger authored Oct 25, 2023
1 parent 289aa0f commit a6b61d2
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 55 deletions.
9 changes: 9 additions & 0 deletions feature-libs/quote/assets/translations/en/quote.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ export const quote = {
save: 'Save quote header information',
},
},
price: {
hint: 'No taxes are included in the total',
orderDiscount: 'Order Discount',
productDiscount: 'Item Discount',
quoteDiscount: 'Quote Discount',
subtotal: 'Subtotal',
title: 'Order Summary',
total: 'Total',
},
overview: {
id: 'Quote ID',
status: 'Status',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
<ng-container
*ngIf="quoteDetails$ | async as quoteDetails; else ghostQuoteSummary"
>
<ng-template
[cxOutlet]="cartOutlets.ORDER_SUMMARY"
[cxOutletContext]="quoteDetails"
<div class="cx-price-heading">
{{ 'quote.header.price.title' | cxTranslate }}
</div>
<div class="cx-price-row">
<div class="cx-price-label">
{{ 'quote.header.price.subtotal' | cxTranslate }}
</div>
<div class="cx-price-amount">
{{ quoteDetails.totalPrice?.formattedValue }}
</div>
</div>

<div
*ngIf="hasNonZeroPriceValue(quoteDetails.orderDiscounts)"
class="cx-price-row cx-price-savings"
>
</ng-template>
<div class="cx-price-label">
{{ 'quote.header.price.orderDiscount' | cxTranslate }}
</div>
<div class="cx-price-amount">
{{ quoteDetails.orderDiscounts.formattedValue }}
</div>
</div>

<div
*ngIf="hasNonZeroPriceValue(quoteDetails.productDiscounts)"
class="cx-price-row cx-price-savings"
>
<div class="cx-price-label">
{{ 'quote.header.price.productDiscount' | cxTranslate }}
</div>
<div class="cx-price-amount">
{{ quoteDetails.productDiscounts?.formattedValue }}
</div>
</div>

<div
*ngIf="hasNonZeroPriceValue(quoteDetails.quoteDiscounts)"
class="cx-price-row cx-price-savings"
>
<div class="cx-price-label">
{{ 'quote.header.price.quoteDiscount' | cxTranslate }}
</div>
<div class="cx-price-amount">
{{ quoteDetails.quoteDiscounts?.formattedValue }}
</div>
</div>

<div class="cx-price-row cx-price-total">
<div class="cx-price-label">
{{ 'quote.header.price.total' | cxTranslate }}
</div>
<div class="cx-price-amount">
{{ quoteDetails.totalPrice?.formattedValue }}
</div>
</div>
<div class="cx-price-footer">
*{{ 'quote.header.price.hint' | cxTranslate }}
</div>
</ng-container>

<ng-template #ghostQuoteSummary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
import { Directive, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { I18nTestingModule, Price } from '@spartacus/core';
import {
Quote,
QuoteActionType,
QuoteFacade,
QuoteState,
} from '@spartacus/quote/root';
import { OutletDirective } from '@spartacus/storefront';
import { I18nTestingModule } from '@spartacus/core';
import { Quote, QuoteFacade } from '@spartacus/quote/root';
import { BehaviorSubject, NEVER, Observable } from 'rxjs';
import { createEmptyQuote } from '../../../core/testing/quote-test-utils';
import { CommonQuoteTestUtilsService } from '../../testing/common-quote-test-utils.service';
import { CommonQuoteTestUtilsService as TestUtil } from '../../testing/common-quote-test-utils.service';
import { QuoteHeaderPriceComponent } from './quote-header-price.component';

const cartId = '1234';
const quoteCode = '3333';
const threshold = 20;
const totalPrice: Price = { value: threshold + 1 };

const quote: Quote = {
...createEmptyQuote(),
allowedActions: [
{ type: QuoteActionType.EDIT, isPrimary: false },
{ type: QuoteActionType.REQUOTE, isPrimary: true },
],
state: QuoteState.BUYER_DRAFT,
cartId: cartId,
code: quoteCode,
threshold: threshold,
totalPrice: totalPrice,
};

const mockQuoteDetails$ = new BehaviorSubject<Quote>(quote);
Expand All @@ -39,24 +18,15 @@ class MockCommerceQuotesFacade implements Partial<QuoteFacade> {
}
}

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

describe('QuoteHeaderPriceComponent', () => {
let fixture: ComponentFixture<QuoteHeaderPriceComponent>;
let htmlElem: HTMLElement;
let component: QuoteHeaderPriceComponent;
let facade: QuoteFacade;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule],
declarations: [QuoteHeaderPriceComponent, MockOutletDirective],
declarations: [QuoteHeaderPriceComponent],
providers: [
{
provide: QuoteFacade,
Expand All @@ -70,53 +40,155 @@ describe('QuoteHeaderPriceComponent', () => {
fixture = TestBed.createComponent(QuoteHeaderPriceComponent);
htmlElem = fixture.nativeElement;
component = fixture.componentInstance;
facade = TestBed.inject(QuoteFacade);
mockQuoteDetails$.next(quote);
withPrices();
fixture.detectChanges();
});

function withPrices() {
quote.totalPrice = { value: 1000, formattedValue: '$1,000.00' };
quote.orderDiscounts = { value: 5.99, formattedValue: '$5.99' };
quote.productDiscounts = { value: 50, formattedValue: '$50.00' };
quote.quoteDiscounts = { value: 100, formattedValue: '$100.00' };
}

it('should create component', () => {
expect(component).toBeDefined();
expect(facade).toBeDefined();
});

it('should display all prices and discounts when present', () => {
TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-price-row',
5
);
TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-price-savings',
3
);

TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.subtotal $1,000.00',
0
);
TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.orderDiscount $5.99',
1
);
TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.productDiscount $50.00',
2
);
TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.quoteDiscount $100.00',
3
);
TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.total $1,000.00',
4
);
});

it('should display only totals when discounts are zero', () => {
quote.orderDiscounts = {};
quote.productDiscounts = undefined;
quote.quoteDiscounts = { value: 0, formattedValue: '$0.00' };
fixture.detectChanges();

TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-price-row',
2
);
TestUtil.expectElementNotPresent(expect, htmlElem, '.cx-price-savings');

TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.subtotal $1,000.00',
0
);

TestUtil.expectElementToContainText(
expect,
htmlElem,
'.cx-price-row',
'.total $1,000.00',
1
);
});

describe('hasNonZeroPriceValue', () => {
it('should return true if price value is present', () => {
expect(component.hasNonZeroPriceValue({ value: 99.99 })).toBe(true);
});

it('should return false if price is not present', () => {
expect(component.hasNonZeroPriceValue(undefined)).toBe(false);
});

it('should return false if price value is not present', () => {
expect(component.hasNonZeroPriceValue({})).toBe(false);
});

it('should return false if price value is zero', () => {
expect(component.hasNonZeroPriceValue({ value: 0.0 })).toBe(false);
});
});

describe('Ghost animation', () => {
it('should render view for ghost animation', () => {
component.quoteDetails$ = NEVER;
fixture.detectChanges();

CommonQuoteTestUtilsService.expectElementPresent(
TestUtil.expectElementPresent(
expect,
htmlElem,
'.cx-ghost-summary-heading'
);

CommonQuoteTestUtilsService.expectElementPresent(
expect,
htmlElem,
'.cx-ghost-title'
);
TestUtil.expectElementPresent(expect, htmlElem, '.cx-ghost-title');

CommonQuoteTestUtilsService.expectElementPresent(
TestUtil.expectElementPresent(
expect,
htmlElem,
'.cx-ghost-summary-partials'
);

CommonQuoteTestUtilsService.expectNumberOfElementsPresent(
TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-ghost-row',
4
);

CommonQuoteTestUtilsService.expectNumberOfElementsPresent(
TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-ghost-summary-label',
4
);

CommonQuoteTestUtilsService.expectNumberOfElementsPresent(
TestUtil.expectNumberOfElementsPresent(
expect,
htmlElem,
'.cx-ghost-summary-amount',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { Component, inject } from '@angular/core';
import { CartOutlets } from '@spartacus/cart/base/root';
import { Price } from '@spartacus/core';
import { QuoteFacade } from '@spartacus/quote/root';

@Component({
Expand All @@ -15,6 +15,15 @@ import { QuoteFacade } from '@spartacus/quote/root';
export class QuoteHeaderPriceComponent {
protected quoteFacade = inject(QuoteFacade);

readonly cartOutlets = CartOutlets;
quoteDetails$ = this.quoteFacade.getQuoteDetails();

/**
* Checks whether the price has a non-zero value
*
* @param price to check
* @returns true, only if the price has a non zero value
*/
hasNonZeroPriceValue(price?: Price): boolean {
return !!price && !!price.value && price.value > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {
I18nModule,
provideDefaultConfig,
} from '@spartacus/core';
import { OutletModule } from '@spartacus/storefront';
import { QuoteHeaderPriceComponent } from './quote-header-price.component';

@NgModule({
imports: [CommonModule, I18nModule, OutletModule],
imports: [CommonModule, I18nModule],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
Expand Down
22 changes: 22 additions & 0 deletions feature-libs/quote/styles/_quote-header-price.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
%cx-quote-header-price {
> div {
padding-block-end: 0.5rem;
}
.cx-price-heading {
@include type('4');
font-weight: var(--cx-font-weight-bold);
}
.cx-price-footer {
font-style: italic;
}
.cx-price-row {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
}
.cx-price-savings {
color: var(--cx-color-success);
}
.cx-price-total {
font-weight: var(--cx-font-weight-bold);
}

&:not(:empty) {
.cx-ghost-title,
.cx-ghost-summary-label,
Expand Down

0 comments on commit a6b61d2

Please sign in to comment.