Skip to content

Commit

Permalink
feat: Quote Header Comment [CXSPA-3982] (#17627)
Browse files Browse the repository at this point in the history
Co-authored-by: Moritz Schaefer <[email protected]>
Co-authored-by: Christoph Hinssen <[email protected]>
  • Loading branch information
3 people authored Jul 19, 2023
1 parent 170e08d commit a8f32a8
Show file tree
Hide file tree
Showing 43 changed files with 821 additions and 297 deletions.
5 changes: 4 additions & 1 deletion feature-libs/quote/assets/translations/en/quote.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export const quote = {
status: 'Status',
creationSuccess: 'Quote #{{ code }} created successfully',
cart: 'Cart',
contactVendor: 'Contact Vendor',
},
comments: {
title: 'Contact',
invalidComment: 'Invalid Input - Please type again...',
},
details: {
code: 'Quote ID',
Expand Down
11 changes: 11 additions & 0 deletions feature-libs/quote/components/config/augmented-config.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { QuoteUIConfig } from './quote-ui.config';

declare module '@spartacus/core' {
interface Config extends QuoteUIConfig {}
}
11 changes: 11 additions & 0 deletions feature-libs/quote/components/config/default-quote-ui.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { QuoteUIConfig } from './quote-ui.config';

export const defaultQuoteUIConfig: QuoteUIConfig = {
quote: { maxCharsForComments: 1000 },
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
* SPDX-License-Identifier: Apache-2.0
*/

export * from './quote-details-vendor-contact.component';
export * from './quote-details-vendor-contact.module';
export * from './quote-ui.config';
import './augmented-config.model';
20 changes: 20 additions & 0 deletions feature-libs/quote/components/config/quote-ui.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Injectable } from '@angular/core';
import { Config } from '@spartacus/core';

export interface QuoteUIConfigFragment {
maxCharsForComments?: number;
}

@Injectable({
providedIn: 'root',
useExisting: Config,
})
export abstract class QuoteUIConfig {
quote?: QuoteUIConfigFragment;
}
8 changes: 8 additions & 0 deletions feature-libs/quote/components/details/comment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

export * from './quote-details-comment.component';
export * from './quote-details-comment.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<ng-container *ngIf="quoteDetails$ | async as quoteDetails">
<div class="quote-comment-toggle" (click)="showComments = !showComments">
<cx-icon
aria-hidden="false"
[type]="showComments ? iconTypes.CARET_UP : iconTypes.CARET_DOWN"
>
</cx-icon>
<span class="quote-comment-text">{{
'quote.comments.title' | cxTranslate
}}</span>
</div>
<div *ngIf="showComments">
<cx-messaging
[messageEvents$]="messageEvents$"
[messagingConfigs]="messagingConfigs"
(send)="onSend($event, quoteDetails.code)"
></cx-messaging>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { Component, Input } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { EventService, I18nTestingModule } from '@spartacus/core';
import {
Comment,
Quote,
QuoteDetailsReloadQueryEvent,
QuoteFacade,
} from '@spartacus/quote/root';
import {
ICON_TYPE,
MessagingComponent,
MessagingConfigs,
} from '@spartacus/storefront';
import { cold } from 'jasmine-marbles';
import { Observable, of, throwError } from 'rxjs';
import { createEmptyQuote } from '../../../core/testing/quote-test-utils';
import { QuoteUIConfig } from '../../config';
import { QuoteDetailsCommentComponent } from './quote-details-comment.component';

const QUOTE_CODE = 'q123';

@Component({
selector: 'cx-messaging',
template: '',
providers: [
{ provide: MessagingComponent, useClass: MockCxMessagingComponent },
],
})
class MockCxMessagingComponent {
@Input() messageEvents$: Observable<Array<MessageEvent>>;
@Input() messagingConfigs?: MessagingConfigs;
resetForm(): void {}
}

@Component({
selector: 'cx-icon',
template: '',
})
class MockCxIconComponent {
@Input() type: ICON_TYPE;
}

describe('QuoteDetailsCommentComponent', () => {
let fixture: ComponentFixture<QuoteDetailsCommentComponent>;
let component: QuoteDetailsCommentComponent;
let mockedQuoteFacade: QuoteFacade;
let mockedEventService: EventService;
let quoteUiConfig: QuoteUIConfig;

let quote: Quote;

beforeEach(
waitForAsync(() => {
initTestData();
initMocks();
TestBed.configureTestingModule({
imports: [I18nTestingModule],
declarations: [
QuoteDetailsCommentComponent,
MockCxMessagingComponent,
MockCxIconComponent,
],
providers: [
{
provide: QuoteFacade,
useValue: mockedQuoteFacade,
},
{
provide: EventService,
useValue: mockedEventService,
},
{
provide: QuoteUIConfig,
useValue: quoteUiConfig,
},
],
}).compileComponents();
})
);

beforeEach(() => {
fixture = TestBed.createComponent(QuoteDetailsCommentComponent);
component = fixture.componentInstance;

fixture.detectChanges();
spyOn(component.commentsComponent, 'resetForm');
});

function initTestData() {
quote = createEmptyQuote();
quote.code = QUOTE_CODE;
quoteUiConfig = {
quote: { maxCharsForComments: 5000 },
};
}

function initMocks() {
mockedQuoteFacade = jasmine.createSpyObj('quoteFacade', [
'getQuoteDetails',
'addQuoteComment',
]);
asSpy(mockedQuoteFacade.getQuoteDetails).and.returnValue(of(quote));
asSpy(mockedQuoteFacade.addQuoteComment).and.returnValue(of({}));

mockedEventService = jasmine.createSpyObj('eventService', ['dispatch']);
}

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

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

it('should render the messaging section by default', () => {
expect(fixture.debugElement.query(By.css('cx-messaging'))).not.toBeNull();
});

it('should hide the comments area when clicking the toggle', () => {
clickCommentsToggle(fixture);
expect(fixture.debugElement.query(By.css('cx-messaging'))).toBeNull();
});

it('should show the comments area when clicking the toggle', () => {
component.showComments = false;
clickCommentsToggle(fixture);
expect(fixture.debugElement.query(By.css('cx-messaging'))).not.toBeNull();
});

it('should pipe empty quote comments to empty message events', () => {
component.messageEvents$
.subscribe((messageEvent) => {
expect(messageEvent.length).toBe(0);
})
.unsubscribe();
});

it('should pipe quote comments to message events', () => {
quote.comments = [];
quote.comments.push({});
quote.comments.push({});
component.messageEvents$
.subscribe((messageEvent) => {
expect(messageEvent.length).toBe(2);
})
.unsubscribe();
});

function clickCommentsToggle(
fixture: ComponentFixture<QuoteDetailsCommentComponent>
) {
fixture.debugElement
.query(By.css('.quote-comment-toggle'))
.nativeElement.click();
fixture.detectChanges();
}
describe('messagingConfigs', () => {
it('should be provided', () => {
expect(component.messagingConfigs).toBeDefined();
});
it('should set chars limit to default 1000 when not provided via config', () => {
quoteUiConfig.quote = undefined;
// re-create component so changed config is evaluated
fixture = TestBed.createComponent(QuoteDetailsCommentComponent);
expect(fixture.componentInstance.messagingConfigs.charactersLimit).toBe(
1000
);
});
it('should set chars limit from config', () => {
expect(component.messagingConfigs.charactersLimit).toBe(5000);
});
it('should define a date format', () => {
expect(component.messagingConfigs.dateFormat).toBe(
'MMMM d, yyyy h:mm aa'
);
});
it('should display add section for editable quotes', () => {
quote.isEditable = true;
(component.messagingConfigs.displayAddMessageSection ?? of(false))
.subscribe((showAddSection) => {
expect(showAddSection).toBe(true);
})
.unsubscribe();
});
it('should hide display add section for not editable quotes', () => {
quote.isEditable = false;
(component.messagingConfigs.displayAddMessageSection ?? of(true))
.subscribe((showAddSection) => {
expect(showAddSection).toBe(false);
})
.unsubscribe();
});
});

describe('mapCommentToMessageEvent', () => {
const comment = {
text: 'comment text',
creationDate: new Date('2022-10-03T17:33:45'),
fromCustomer: false,
author: { uid: 'cust_1', name: 'John Doe' },
};

function mapCommentToMessageEvent(comment: Comment) {
return component['mapCommentToMessageEvent'](comment);
}

it('should map comment text', () => {
expect(mapCommentToMessageEvent(comment).text).toEqual('comment text');
});
it('should map creation date', () => {
expect(mapCommentToMessageEvent(comment).createdAt).toContain(
'Mon Oct 03 2022 17:33:45'
);
});
it('should map author', () => {
expect(mapCommentToMessageEvent(comment).author).toEqual('John Doe');
});
it('should map fromCustomer to not rightAligned', () => {
comment.fromCustomer = true;
expect(mapCommentToMessageEvent(comment).rightAlign).toEqual(false);
});
it('should map not fromCustomer to rightAligned', () => {
comment.fromCustomer = false;
expect(mapCommentToMessageEvent(comment).rightAlign).toEqual(true);
});
it("shouldn't map anything to code", () => {
expect(mapCommentToMessageEvent(comment).code).toBeUndefined();
});
it("shouldn't map anything to attachments", () => {
expect(mapCommentToMessageEvent(comment).attachments).toBeUndefined();
});
});

describe('onSend', () => {
it('should add a quote comment with the given text', () => {
component.onSend({ message: 'test comment' }, QUOTE_CODE);
expect(mockedQuoteFacade.addQuoteComment).toHaveBeenCalledWith(
QUOTE_CODE,
{
text: 'test comment',
}
);
});
it('should refresh the quote to display the just added comment', () => {
component.onSend({ message: 'test comment' }, QUOTE_CODE);
expect(mockedEventService.dispatch).toHaveBeenCalledWith(
{},
QuoteDetailsReloadQueryEvent
);
});
it('should reset message input text', () => {
component.onSend({ message: 'test comment' }, QUOTE_CODE);
expect(component.commentsComponent.resetForm).toHaveBeenCalled();
expect(component.messagingConfigs.newMessagePlaceHolder).toBeUndefined();
});
it('should handle errors', () => {
asSpy(mockedQuoteFacade.addQuoteComment).and.returnValue(
throwError(new Error('test error'))
);
component.onSend({ message: 'test comment' }, QUOTE_CODE);
expect(component.commentsComponent.resetForm).toHaveBeenCalled();
expect(component.messagingConfigs.newMessagePlaceHolder).toEqual(
'quote.comments.invalidComment'
);
});
});

describe('prepareMessageEvents', () => {
it('should be able to handle undefined comments in model', () => {
const eventsObs = component['prepareMessageEvents']();
expect(eventsObs).toBeObservable(cold('(a|)', { a: [] }));
});
});
});
Loading

0 comments on commit a8f32a8

Please sign in to comment.