diff --git a/apps/demo/src/app/newsletter/newsletter.component.spec.ts b/apps/demo/src/app/newsletter/newsletter.component.spec.ts index b270979358..e44181d6ee 100644 --- a/apps/demo/src/app/newsletter/newsletter.component.spec.ts +++ b/apps/demo/src/app/newsletter/newsletter.component.spec.ts @@ -4,6 +4,7 @@ import { TestBed, } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; import { DaffContainerModule } from '@daffodil/design/container'; import { @@ -52,7 +53,7 @@ describe('NewsletterComponent', () => { facade.success$.next(false); fixture.detectChanges(); - newsletterElement = fixture.debugElement.nativeElement.querySelector('.demo-newsletter__right'); + newsletterElement = fixture.debugElement.query(By.css('.demo-newsletter__right')); }); it('should render class .demo-newsletter__right', () => { @@ -114,7 +115,7 @@ describe('NewsletterComponent', () => { beforeEach(() => { facade.loading$.next(false); facade.success$.next(false); - facade.error$.next({ code: 'code', message: 'message' }); + facade.error$.next([{ code: 'code', message: 'message' }]); fixture.detectChanges(); newsletterElement = fixture.debugElement.nativeElement.querySelector('.demo-newsletter__retry'); diff --git a/apps/demo/src/app/newsletter/newsletter.component.ts b/apps/demo/src/app/newsletter/newsletter.component.ts index e157aaa4a5..b77134868f 100644 --- a/apps/demo/src/app/newsletter/newsletter.component.ts +++ b/apps/demo/src/app/newsletter/newsletter.component.ts @@ -36,23 +36,23 @@ export class NewsletterComponent implements OnInit { ngOnInit() { this.hasError$ = this.error$.pipe( - map(error => !!error), + map(error => error.length > 0), ); } onNewsletterSubmit() { if (this.email.valid) { - this.newsletterFacade.dispatch(new DaffNewsletterSubscribe(this._makeSubmission(this.email.value))); + this.newsletterFacade.dispatch(new DaffNewsletterSubscribe(this._makeSubmission(this.email.value))); } } onNewsletterCancel() { this.newsletterFacade.dispatch(new DaffNewsletterCancel()); } onNewsletterRetry() { - this.newsletterFacade.dispatch(new DaffNewsletterRetry(this._makeSubmission(this.email.value))); + this.newsletterFacade.dispatch(new DaffNewsletterRetry(this._makeSubmission(this.email.value))); } private _makeSubmission(email: string): DaffNewsletterSubmission { - return { email }; + return email; } } diff --git a/libs/contact/driver/hubspot/ng-package.prod.json b/libs/contact/driver/hubspot/ng-package.prod.json deleted file mode 100644 index 81c5510524..0000000000 --- a/libs/contact/driver/hubspot/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/driver/hubspot", - "lib": { - "entryFile": "src/index.ts" - } -} diff --git a/libs/contact/driver/hubspot/src/config/contact-config.interface.ts b/libs/contact/driver/hubspot/src/config/contact-config.interface.ts index c8059108eb..6bfda47a92 100644 --- a/libs/contact/driver/hubspot/src/config/contact-config.interface.ts +++ b/libs/contact/driver/hubspot/src/config/contact-config.interface.ts @@ -1,7 +1,10 @@ -import { InjectionToken } from '@angular/core'; - +import { createConfigInjectionToken } from '@daffodil/core'; import { DaffHubspotConfig } from '@daffodil/driver/hubspot'; -export const DaffContactConfigToken = new InjectionToken( - 'DaffContactConfig', -); +export const { + /** + * The injection token that holds the configuration of the hubspot contact driver. + */ + token: DaffContactHubspotConfig, + provider: provideDaffContactHubspotConfig, +} = createConfigInjectionToken(null, 'DaffContactHubspotConfig'); diff --git a/libs/contact/driver/hubspot/src/contact.service.spec.ts b/libs/contact/driver/hubspot/src/contact.service.spec.ts index f1b60d325d..3a0fd1cd65 100644 --- a/libs/contact/driver/hubspot/src/contact.service.spec.ts +++ b/libs/contact/driver/hubspot/src/contact.service.spec.ts @@ -5,39 +5,40 @@ import { } from 'jasmine-marbles'; import { Observable } from 'rxjs'; -import { DaffContactDriver } from '@daffodil/contact/driver'; +import { HubspotResponse } from '@daffodil/driver/hubspot'; import { DaffContactHubspotService } from './contact.service'; import { DAFF_CONTACT_HUBSPOT_FORMS_TOKEN } from './token/hubspot-forms.token'; -describe('DaffContactHubspotService', () => { - let contactService; +const stubHubspotResponse: HubspotResponse = { inlineMessage: '123', errors: []}; + +describe('@daffodil/contact/driver/hubspot | DaffContactHubspotService', () => { + let service: DaffContactHubspotService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ - { provide: DaffContactDriver, useClass: DaffContactHubspotService }, { provide: DAFF_CONTACT_HUBSPOT_FORMS_TOKEN, useValue: { - submit: (): Observable => hot('--a', { a: { test: '123' }}), + submit: (): Observable => hot('--a', { a: stubHubspotResponse }), }, }, ], }); - contactService = TestBed.inject(DaffContactDriver); + service = TestBed.inject(DaffContactHubspotService); }); it('should be created', () => { - expect(contactService).toBeTruthy(); + expect(service).toBeTruthy(); }); describe('when sending', () => { - it('should return an observable of HubspotResponse', () => { + it('should return an observable of DaffContactResponse', () => { const payload = { email: 'email@email.edu' }; - const expected = cold('--b', { b: { test: '123' }}); - expect(contactService.send(payload)).toBeObservable(expected); + const expected = cold('--b', { b: { message: '123' }}); + expect(service.send(payload)).toBeObservable(expected); }); }); }); diff --git a/libs/contact/driver/hubspot/src/contact.service.ts b/libs/contact/driver/hubspot/src/contact.service.ts index a95f98c332..a99e1e1b30 100644 --- a/libs/contact/driver/hubspot/src/contact.service.ts +++ b/libs/contact/driver/hubspot/src/contact.service.ts @@ -3,9 +3,13 @@ import { Injectable, } from '@angular/core'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; -import { DaffContactUnion } from '@daffodil/contact'; -import { DaffContactServiceInterface } from '@daffodil/contact/driver'; +import { + DaffContactRequest, + DaffContactResponse, + DaffContactServiceInterface, +} from '@daffodil/contact/driver'; import { DaffHubspotFormsService } from '@daffodil/driver/hubspot'; import { DAFF_CONTACT_HUBSPOT_FORMS_TOKEN } from './token/hubspot-forms.token'; @@ -13,12 +17,13 @@ import { DAFF_CONTACT_HUBSPOT_FORMS_TOKEN } from './token/hubspot-forms.token'; /** * @inheritdoc */ -@Injectable() -export class DaffContactHubspotService -implements DaffContactServiceInterface { +@Injectable({ + providedIn: 'root', +}) +export class DaffContactHubspotService implements DaffContactServiceInterface { constructor(@Inject(DAFF_CONTACT_HUBSPOT_FORMS_TOKEN) private hubspotService: DaffHubspotFormsService) {} - send(payload: DaffContactUnion): Observable { - return this.hubspotService.submit(payload); + send(payload: DaffContactRequest): Observable { + return this.hubspotService.submit(payload).pipe(map((r) => ({ message: r.inlineMessage }))); } } diff --git a/libs/contact/driver/hubspot/src/hubspot-driver.module.ts b/libs/contact/driver/hubspot/src/hubspot-driver.module.ts index 39e9691325..7044c33f4a 100644 --- a/libs/contact/driver/hubspot/src/hubspot-driver.module.ts +++ b/libs/contact/driver/hubspot/src/hubspot-driver.module.ts @@ -7,7 +7,7 @@ import { import { DaffContactDriver } from '@daffodil/contact/driver'; import { DaffHubspotConfig } from '@daffodil/driver/hubspot'; -import { DaffContactConfigToken } from './config/contact-config.interface'; +import { provideDaffContactHubspotConfig } from './config/contact-config.interface'; import { DaffContactHubspotService } from './contact.service'; @NgModule({ @@ -20,8 +20,8 @@ export class DaffContactHubSpotDriverModule { return { ngModule: DaffContactHubSpotDriverModule, providers: [ - { provide: DaffContactDriver, useClass: DaffContactHubspotService }, - { provide: DaffContactConfigToken, useValue: config }, + { provide: DaffContactDriver, useExisting: DaffContactHubspotService }, + provideDaffContactHubspotConfig(config), ], }; } diff --git a/libs/contact/driver/hubspot/src/token/hubspot-forms.token.ts b/libs/contact/driver/hubspot/src/token/hubspot-forms.token.ts index 39feb94b53..089a5682b7 100644 --- a/libs/contact/driver/hubspot/src/token/hubspot-forms.token.ts +++ b/libs/contact/driver/hubspot/src/token/hubspot-forms.token.ts @@ -8,19 +8,23 @@ import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { - DaffHubspotFormsService, daffHubspotFormsServiceFactory, + DaffHubspotFormsInterface, } from '@daffodil/driver/hubspot'; -import { DaffContactConfigToken } from '../config/contact-config.interface'; +import { DaffContactHubspotConfig } from '../config/contact-config.interface'; -export const DAFF_CONTACT_HUBSPOT_FORMS_TOKEN = new InjectionToken('DAFF_CONTACT_HUBSPOT_FORMS_TOKEN', +/** + * The InjectionToken that holds the Hubspot Forms Service + * used by the ContactDriver to send submissions to Hubspot. + */ +export const DAFF_CONTACT_HUBSPOT_FORMS_TOKEN = new InjectionToken('DAFF_CONTACT_HUBSPOT_FORMS_TOKEN', { - providedIn: 'root', factory: () => daffHubspotFormsServiceFactory( + factory: () => daffHubspotFormsServiceFactory( inject(HttpClient), inject(DOCUMENT), inject(Router), inject(Title), - inject(DaffContactConfigToken), + inject(DaffContactHubspotConfig), ), }); diff --git a/libs/contact/driver/in-memory/ng-package.prod.json b/libs/contact/driver/in-memory/ng-package.prod.json deleted file mode 100644 index cfb9725203..0000000000 --- a/libs/contact/driver/in-memory/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/driver/in-memory", - "lib": { - "entryFile": "src/index.ts" - } -} diff --git a/libs/contact/driver/in-memory/src/backend/contact.service.spec.ts b/libs/contact/driver/in-memory/src/backend/contact.service.spec.ts new file mode 100644 index 0000000000..f7d5a9533d --- /dev/null +++ b/libs/contact/driver/in-memory/src/backend/contact.service.spec.ts @@ -0,0 +1,62 @@ +import { TestBed } from '@angular/core/testing'; +import { STATUS } from 'angular-in-memory-web-api'; + +import { DaffInMemoryBackendContactService } from './contact.service'; + +describe('@daffodil/contact/driver/in-memory | DaffInMemoryBackendContactService', () => { + let service: DaffInMemoryBackendContactService; + let reqInfoStub; + let result; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DaffInMemoryBackendContactService], + }); + + service = TestBed.inject(DaffInMemoryBackendContactService); + + reqInfoStub = { + req: {}, + utils: { + createResponse$: f => f(), + getJsonBody: req => req.body, + }, + }; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('on intializiaton', () => { + beforeEach(() => { + result = service.createDb(); + }); + + it('should intialize empty on createDb', () => { + expect(result.submissions).toEqual([]); + }); + }); + + it('should validate that its not empty', () => { + reqInfoStub.req.body = undefined; + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.BAD_REQUEST); + expect(result.statusText).toEqual('Payload is undefined'); + }); + + it('should validate that it doesnt already exist', () => { + reqInfoStub.req.body = { email: 'test@test.com' }; + service.post(reqInfoStub); + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.BAD_REQUEST); + expect(result.statusText).toEqual('Already contains submission'); + }); + + it('should be able to submit a valid form', () => { + reqInfoStub.req.body = { email: 'new@test.com' }; + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.OK); + expect(result.body).toEqual({ message: 'Success!' }); + }); +}); diff --git a/libs/contact/driver/in-memory/src/backend/contact.service.ts b/libs/contact/driver/in-memory/src/backend/contact.service.ts new file mode 100644 index 0000000000..0c01d35fb9 --- /dev/null +++ b/libs/contact/driver/in-memory/src/backend/contact.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { + InMemoryDbService, + RequestInfo, + STATUS, +} from 'angular-in-memory-web-api'; +import { Observable } from 'rxjs'; + +import { DaffContactRequest } from '@daffodil/contact/driver'; +import { DaffContactResponse } from '@daffodil/contact/driver'; +import { DaffInMemorySingleRouteableBackend } from '@daffodil/driver/in-memory'; + +import { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from '../collection-name.const'; + +@Injectable({ + providedIn: 'root', +}) +export class DaffInMemoryBackendContactService implements InMemoryDbService, DaffInMemorySingleRouteableBackend { + readonly collectionName = DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME; + + submissions: DaffContactRequest[] = []; + + createDb() { + return { + submissions: this.submissions, + }; + } + + post(reqInfo: RequestInfo): Observable { + const body = reqInfo.utils.getJsonBody(reqInfo.req); + + return reqInfo.utils.createResponse$(() => { + // validate that its not empty + if (body === undefined) { + return { + status: STATUS.BAD_REQUEST, + statusText: 'Payload is undefined', + }; + // validate that it doesn't already exist + } else if (this.submissions.includes(body)) { + return { + status: STATUS.BAD_REQUEST, + statusText: 'Already contains submission', + }; + } else { + this.submissions.push(body); + return { + status: STATUS.OK, + body: { message: 'Success!' }, + }; + } + }); + } +} diff --git a/libs/contact/driver/in-memory/src/contact.service.spec.ts b/libs/contact/driver/in-memory/src/drivers/contact.service.spec.ts similarity index 100% rename from libs/contact/driver/in-memory/src/contact.service.spec.ts rename to libs/contact/driver/in-memory/src/drivers/contact.service.spec.ts diff --git a/libs/contact/driver/in-memory/src/contact.service.ts b/libs/contact/driver/in-memory/src/drivers/contact.service.ts similarity index 53% rename from libs/contact/driver/in-memory/src/contact.service.ts rename to libs/contact/driver/in-memory/src/drivers/contact.service.ts index a0c236916c..e786a1f119 100644 --- a/libs/contact/driver/in-memory/src/contact.service.ts +++ b/libs/contact/driver/in-memory/src/drivers/contact.service.ts @@ -1,13 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { InMemoryBackendConfig } from 'angular-in-memory-web-api'; -import { Observable } from 'rxjs'; -import { DaffContactUnion } from '@daffodil/contact'; -import { DaffContactServiceInterface } from '@daffodil/contact/driver'; +import { + DaffContactServiceInterface, + DaffContactResponse, +} from '@daffodil/contact/driver'; import { DaffInMemoryDriverBase } from '@daffodil/driver/in-memory'; -import { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from './collection-name.const'; +import { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from '../collection-name.const'; /** * @inheritdoc @@ -15,7 +16,7 @@ import { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from './collection-name.const' @Injectable({ providedIn: 'root', }) -export class DaffInMemoryContactService extends DaffInMemoryDriverBase implements DaffContactServiceInterface{ +export class DaffInMemoryContactService extends DaffInMemoryDriverBase implements DaffContactServiceInterface { constructor( private http: HttpClient, config: InMemoryBackendConfig, @@ -23,7 +24,7 @@ export class DaffInMemoryContactService extends DaffInMemoryDriverBase implement super(config, DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME); } - send(payload: DaffContactUnion): Observable { - return this.http.post(this.url, payload); + send(payload) { + return this.http.post(this.url, payload); } } diff --git a/libs/contact/driver/in-memory/src/in-memory.module.ts b/libs/contact/driver/in-memory/src/drivers/in-memory.module.ts similarity index 87% rename from libs/contact/driver/in-memory/src/in-memory.module.ts rename to libs/contact/driver/in-memory/src/drivers/in-memory.module.ts index 04b3da8afb..fa17005da4 100644 --- a/libs/contact/driver/in-memory/src/in-memory.module.ts +++ b/libs/contact/driver/in-memory/src/drivers/in-memory.module.ts @@ -8,7 +8,7 @@ import { DaffContactDriver } from '@daffodil/contact/driver'; import { provideDaffInMemoryBackends } from '@daffodil/driver/in-memory'; import { DaffInMemoryContactService } from './contact.service'; -import { DaffInMemoryBackendContactService } from './in-memory-backend/contact-in-memory-backend.service'; +import { DaffInMemoryBackendContactService } from '../backend/contact.service'; @NgModule({ imports: [CommonModule], diff --git a/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.spec.ts b/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.spec.ts deleted file mode 100644 index 8fe702a69c..0000000000 --- a/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; - -import { DaffContactUnion } from '@daffodil/contact'; - -import { DaffInMemoryBackendContactService } from './contact-in-memory-backend.service'; - -describe('DaffContactInMemoryBackend', () => { - let contactTestingService; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DaffInMemoryBackendContactService], - }); - contactTestingService = TestBed.inject(DaffInMemoryBackendContactService); - }); - - it('should be created', () => { - expect(contactTestingService).toBeTruthy(); - }); - - describe('on intializiaton', () => { - let result; - - beforeEach(() => { - result = contactTestingService.createDb(); - }); - - it('should intialize empty on createDb', () => { - expect(result.forums).toEqual([]); - }); - - it('should validate that its not empty', () => { - const forumSubmission: DaffContactUnion = undefined; - expect(contactTestingService.post(forumSubmission)).toEqual(Error('Payload is undefined')); - }); - - it('should validate that it doesnt already exist', () => { - - const forumSubmission: DaffContactUnion = { email: 'test@test.com' }; - contactTestingService.post(forumSubmission); - expect(contactTestingService.post(forumSubmission)).toEqual(Error('Already contains submission')); - }); - - it('should be able to submit a valid form', () => { - const forumSubmission: DaffContactUnion = { email: 'new@test.com' }; - contactTestingService.post(forumSubmission).subscribe((resp) => { - expect(resp).toEqual(forumSubmission); - }); - }); - }); -}); diff --git a/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.ts b/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.ts deleted file mode 100644 index 29e137276d..0000000000 --- a/libs/contact/driver/in-memory/src/in-memory-backend/contact-in-memory-backend.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - InMemoryDbService, - RequestInfoUtilities, - ParsedRequestUrl, -} from 'angular-in-memory-web-api'; -import { of } from 'rxjs'; - -import { DaffContactUnion } from '@daffodil/contact'; -import { DaffInMemorySingleRouteableBackend } from '@daffodil/driver/in-memory'; - -import { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from '../collection-name.const'; - -@Injectable({ - providedIn: 'root', -}) -export class DaffInMemoryBackendContactService implements InMemoryDbService, DaffInMemorySingleRouteableBackend { - readonly collectionName = DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME; - - forums: DaffContactUnion[] = []; - - parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl { - return utils.parseRequestUrl(url); - } - - createDb(): any { - return { - forums: this.forums, - }; - } - //validate that its not empty - //validate that it doesn't already exist - post(reqInfo: any): any { - if(reqInfo === undefined){ - return Error('Payload is undefined'); - } else if(this.forums.indexOf(reqInfo.body) !== -1){ - return Error('Already contains submission'); - } else{ - this.forums.push(reqInfo.body); - return of(reqInfo); - } - } -} diff --git a/libs/contact/driver/in-memory/src/public_api.ts b/libs/contact/driver/in-memory/src/public_api.ts index 444b620bb4..f4cf77040a 100644 --- a/libs/contact/driver/in-memory/src/public_api.ts +++ b/libs/contact/driver/in-memory/src/public_api.ts @@ -1,3 +1,3 @@ -export { DaffContactInMemoryDriverModule } from './in-memory.module'; -export { DaffInMemoryBackendContactService } from './in-memory-backend/contact-in-memory-backend.service'; - +export { DaffContactInMemoryDriverModule } from './drivers/in-memory.module'; +export { DaffInMemoryBackendContactService } from './backend/contact.service'; +export { DAFF_CONTACT_IN_MEMORY_COLLECTION_NAME } from './collection-name.const'; diff --git a/libs/contact/driver/ng-package.prod.json b/libs/contact/driver/ng-package.prod.json deleted file mode 100644 index 4b8e298efa..0000000000 --- a/libs/contact/driver/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/driver", - "lib": { - "entryFile": "src/index.ts" - } -} \ No newline at end of file diff --git a/libs/contact/driver/src/interfaces/contact-service.interface.ts b/libs/contact/driver/src/interfaces/contact-service.interface.ts index 98bfeb4a73..f44425177c 100644 --- a/libs/contact/driver/src/interfaces/contact-service.interface.ts +++ b/libs/contact/driver/src/interfaces/contact-service.interface.ts @@ -1,7 +1,20 @@ import { InjectionToken } from '@angular/core'; -import { Observable } from 'rxjs'; +import { + Observable, + of, +} from 'rxjs'; -export const DaffContactDriver = new InjectionToken>('DaffContactDriver'); -export interface DaffContactServiceInterface { - send(payload: T): Observable; +import { DaffContactRequest } from '../model/contact-request'; +import { DaffContactResponse } from '../model/contact-response'; + +/** + * The injection token that holds the reference to the DaffContactDriver. + */ +export const DaffContactDriver = new InjectionToken('DaffContactDriver'); + +/** + * The interface that a contact driver must implement. + */ +export interface DaffContactServiceInterface { + send(payload: DaffContactRequest): Observable; } diff --git a/libs/contact/driver/src/model/contact-request.ts b/libs/contact/driver/src/model/contact-request.ts new file mode 100644 index 0000000000..57584bfe49 --- /dev/null +++ b/libs/contact/driver/src/model/contact-request.ts @@ -0,0 +1,16 @@ +import { DaffContactUnion } from '@daffodil/contact'; + +/** + * A weak type, specifying the fields that drivers are be guaranteed to act upon. + */ +export interface DaffContactBlueprintRequest { + name?: string; + email: string; + phone?: string; + message?: string; +} + +/** + * The type used by the DaffContactDriver to send requests to implementing services. + */ +export type DaffContactRequest = DaffContactBlueprintRequest | DaffContactUnion; diff --git a/libs/contact/driver/src/model/contact-response.ts b/libs/contact/driver/src/model/contact-response.ts new file mode 100644 index 0000000000..ea9575b4ae --- /dev/null +++ b/libs/contact/driver/src/model/contact-response.ts @@ -0,0 +1,9 @@ +/** + * The response object to a successful contact form submission. + */ +export interface DaffContactResponse { + /** + * The message provided by a successful form submission. + */ + message: string; +} diff --git a/libs/contact/driver/src/public_api.ts b/libs/contact/driver/src/public_api.ts index fd4473f8e1..1cebbace2e 100644 --- a/libs/contact/driver/src/public_api.ts +++ b/libs/contact/driver/src/public_api.ts @@ -1 +1,3 @@ export * from './interfaces/contact-service.interface'; +export { DaffContactRequest } from './model/contact-request'; +export { DaffContactResponse } from './model/contact-response'; diff --git a/libs/contact/driver/testing/ng-package.prod.json b/libs/contact/driver/testing/ng-package.prod.json deleted file mode 100644 index 2a4414801e..0000000000 --- a/libs/contact/driver/testing/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/driver/testing", - "lib": { - "entryFile": "src/index.ts" - } -} \ No newline at end of file diff --git a/libs/contact/driver/testing/src/contact.service.spec.ts b/libs/contact/driver/testing/src/contact.service.spec.ts new file mode 100644 index 0000000000..07347c14f0 --- /dev/null +++ b/libs/contact/driver/testing/src/contact.service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { TestScheduler } from 'rxjs/testing'; + +import { DaffTestingContactService } from './contact.service'; + +describe('The DaffTestingContactService', () => { + let service: DaffTestingContactService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + DaffTestingContactService, + ], + }); + service = TestBed.inject(DaffTestingContactService); + }); + + it('should be created',() =>{ + expect(service).toBeTruthy(); + }); + + describe('when sending', () => { + it('should return an observable of DaffContactResponse', () => { + + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + const payload = { email: 'email@email.edu' }; + const expected = { a: { message: 'success' }}; + + const send = service.send(payload); + + testScheduler.run((helpers) => { + const { expectObservable } = helpers; + + expectObservable(send).toBe('----------(a|)', expected); + }); + }); + }); +}); diff --git a/libs/contact/driver/testing/src/contact.service.ts b/libs/contact/driver/testing/src/contact.service.ts index 295a44d9ac..b3d6e78af1 100644 --- a/libs/contact/driver/testing/src/contact.service.ts +++ b/libs/contact/driver/testing/src/contact.service.ts @@ -1,12 +1,15 @@ import { Injectable } from '@angular/core'; import { - of, Observable, + of, } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { DaffContactUnion } from '@daffodil/contact'; -import { DaffContactServiceInterface } from '@daffodil/contact/driver'; +import { + DaffContactRequest, + DaffContactResponse, + DaffContactServiceInterface, +} from '@daffodil/contact/driver'; /** * @inheritdoc @@ -14,8 +17,8 @@ import { DaffContactServiceInterface } from '@daffodil/contact/driver'; @Injectable({ providedIn: 'root', }) -export class DaffTestingContactService implements DaffContactServiceInterface{ - send(payload: DaffContactUnion): Observable{ - return of('Success').pipe(delay(10)); +export class DaffTestingContactService implements DaffContactServiceInterface { + send(payload: DaffContactRequest): Observable { + return of({ message: 'success' }).pipe(delay(10)); } } diff --git a/libs/contact/integration-tests/drivers/hubspot.spec.ts b/libs/contact/integration-tests/drivers/hubspot.spec.ts index 3ba9b6b03d..eda93a786b 100644 --- a/libs/contact/integration-tests/drivers/hubspot.spec.ts +++ b/libs/contact/integration-tests/drivers/hubspot.spec.ts @@ -8,25 +8,36 @@ import { } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { of } from 'rxjs'; -import { DaffContactDriver } from '@daffodil/contact/driver'; +import { + DaffContactDriver, + DaffContactServiceInterface, +} from '@daffodil/contact/driver'; import { DaffContactHubSpotDriverModule } from '@daffodil/contact/driver/hubspot'; +import { HubspotResponse } from '@daffodil/driver/hubspot'; + +const stubHubspotResponse: HubspotResponse = { inlineMessage: 'Success!', errors: []}; describe('DaffContactHubspotDriver', () => { - let contactService; + let service: DaffContactServiceInterface; let httpMock: HttpTestingController; + beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule, + imports: [ + RouterTestingModule, DaffContactHubSpotDriverModule.forRoot({ portalId: '123123', guid: '123123', - })], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }), + ], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], }); httpMock = TestBed.inject(HttpTestingController); - contactService = TestBed.inject(DaffContactDriver); + service = TestBed.inject(DaffContactDriver); }); afterEach(() => { @@ -34,24 +45,23 @@ describe('DaffContactHubspotDriver', () => { }); it('should be created', () => { - expect(contactService).toBeTruthy(); + expect(service).toBeTruthy(); }); describe('when sending', () => { it('should send a submission', () => { - const forumSubmission = { email: 'test@email.com' }; - contactService.send(forumSubmission).subscribe((resp) => { - expect(resp).toEqual(forumSubmission); + const submission = { email: 'test@email.com' }; + service.send(submission).subscribe((resp) => { + expect(resp).toEqual({ message: 'Success!' }); }); const req = httpMock.expectOne( 'https://api.hsforms.com/submissions/v3/integration/submit/123123/123123', ); expect(req.request.body).toEqual(jasmine.objectContaining({ fields: [Object({ name: 'email', value: 'test@email.com' })], - context: Object({ hutk: null, pageUri: '/', pageName: jasmine.any(String) }), + context: jasmine.objectContaining({ pageUri: '/', pageName: jasmine.any(String) }), })); - req.flush(forumSubmission); - httpMock.verify(); + req.flush(stubHubspotResponse); }); }); diff --git a/libs/contact/state/ng-package.prod.json b/libs/contact/state/ng-package.prod.json deleted file mode 100644 index ea69e1d3c1..0000000000 --- a/libs/contact/state/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/state", - "lib": { - "entryFile": "src/index.ts" - } -} \ No newline at end of file diff --git a/libs/contact/state/src/actions/contact.actions.ts b/libs/contact/state/src/actions/contact.actions.ts index 3ae2ea1ca0..fcc07c4793 100644 --- a/libs/contact/state/src/actions/contact.actions.ts +++ b/libs/contact/state/src/actions/contact.actions.ts @@ -1,46 +1,78 @@ import { Action } from '@ngrx/store'; -import { DaffStateError } from '@daffodil/core/state'; +import { DaffContactRequest } from '@daffodil/contact/driver'; +import { + DaffFailureAction, + DaffStateError, +} from '@daffodil/core/state'; export enum DaffContactActionTypes { - ContactSubmitAction = '[@daffodil/contact] Contact Submit Action', - ContactCancelAction = '[@daffodil/contact] Contact Cancel Action', - ContactSuccessSubmitAction = '[@daffodil/contact] Contact Success Submit Action', - ContactFailedSubmitAction = '[@daffodil/contact] Contact Failed Submit Action', - ContactRetryAction = '[@daffodil/contact] Contact Retry Action', - ContactResetAction = '[@daffodil/contact] Contact Reset Action', + Submit = '[@daffodil/contact] Submit', + Cancel = '[@daffodil/contact] Cancel ', + SubmitSuccess = '[@daffodil/contact] Submit Success', + SubmitFailure = '[@daffodil/contact] Submit Failure', + Retry = '[@daffodil/contact] Retry', + Reset = '[@daffodil/contact] Reset', } -export class DaffContactSubmit implements Action { - readonly type = DaffContactActionTypes.ContactSubmitAction; +/** + * An action triggered upon submitting a contact request. + * + * @param payload - a contact request payload + */ +export class DaffContactSubmit implements Action { + readonly type = DaffContactActionTypes.Submit; - constructor(public payload: T) {} + constructor(public payload: DaffContactRequest) {} } -export class DaffContactRetry implements Action { - readonly type = DaffContactActionTypes.ContactRetryAction; - - constructor(public payload: T) {} +/** + * An action triggered upon success of a contact request submission. + */ +export class DaffContactSubmitSuccess implements Action { + readonly type = DaffContactActionTypes.SubmitSuccess; } -export class DaffContactFailedSubmit implements Action { - readonly type = DaffContactActionTypes.ContactFailedSubmitAction; + +/** + * An action triggered upon failure of a contact request submission. + * + * @param payload - an array of errors + */ +export class DaffContactSubmitFailure implements DaffFailureAction { + readonly type = DaffContactActionTypes.SubmitFailure; constructor(public payload: DaffStateError[]) {} } -export class DaffContactCancel implements Action { - readonly type = DaffContactActionTypes.ContactCancelAction; + +/** + * An action triggered upon resubmitting a contact request. + * + * @param payload - a contact request payload + */ +export class DaffContactRetry implements Action { + readonly type = DaffContactActionTypes.Retry; + + constructor(public payload: DaffContactRequest) {} } -export class DaffContactSuccessSubmit implements Action { - readonly type = DaffContactActionTypes.ContactSuccessSubmitAction; + +/** + * An action triggered upon cancelling a contact request. + */ +export class DaffContactCancel implements Action { + readonly type = DaffContactActionTypes.Cancel; } + +/** + * An action triggered upon resetting of a contact. + */ export class DaffContactReset implements Action { - readonly type = DaffContactActionTypes.ContactResetAction; + readonly type = DaffContactActionTypes.Reset; } -export type DaffContactActions = - | DaffContactSubmit - | DaffContactRetry - | DaffContactFailedSubmit - | DaffContactCancel - | DaffContactSuccessSubmit - | DaffContactReset; +export type DaffContactActions = + | DaffContactSubmit + | DaffContactSubmitSuccess + | DaffContactSubmitFailure + | DaffContactRetry + | DaffContactCancel + | DaffContactReset; diff --git a/libs/contact/state/src/contact.module.ts b/libs/contact/state/src/contact.module.ts index 23324c759f..ef1cd5815e 100644 --- a/libs/contact/state/src/contact.module.ts +++ b/libs/contact/state/src/contact.module.ts @@ -3,12 +3,12 @@ import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { DaffContactEffects } from './effects/contact.effects'; -import { reducer } from './reducers/contact.reducer'; +import { daffContactStateReducer } from './reducers/contact.reducer'; @NgModule({ declarations: [], imports: [ - StoreModule.forFeature('contact', reducer), + StoreModule.forFeature('contact', daffContactStateReducer), EffectsModule.forFeature([DaffContactEffects]), ], providers: [], diff --git a/libs/contact/state/src/effects/contact.effects.spec.ts b/libs/contact/state/src/effects/contact.effects.spec.ts index 850fa24ede..06b59096a7 100644 --- a/libs/contact/state/src/effects/contact.effects.spec.ts +++ b/libs/contact/state/src/effects/contact.effects.spec.ts @@ -1,21 +1,18 @@ import { TestBed } from '@angular/core/testing'; +import { Actions } from '@ngrx/effects'; import { provideMockActions } from '@ngrx/effects/testing'; import { hot, cold, } from 'jasmine-marbles'; -import { - Observable, - of, -} from 'rxjs'; +import { of } from 'rxjs'; -import { DaffContactUnion } from '@daffodil/contact'; import { DaffContactDriver } from '@daffodil/contact/driver'; import { DaffContactTestingDriverModule } from '@daffodil/contact/driver/testing'; import { DaffContactSubmit, - DaffContactSuccessSubmit, - DaffContactFailedSubmit, + DaffContactSubmitSuccess, + DaffContactSubmitFailure, DaffContactRetry, DaffContactCancel, } from '@daffodil/contact/state'; @@ -23,8 +20,8 @@ import { import { DaffContactEffects } from './contact.effects'; describe('DaffContactEffects', () => { - let actions$: Observable; - let effects: DaffContactEffects; + let actions$: Actions; + let effects: DaffContactEffects; const mockForm = { firstName: 'John', lastName: 'Doe' }; let daffContactDriver; @@ -46,7 +43,7 @@ describe('DaffContactEffects', () => { const forumSubmit = new DaffContactSubmit(mockForm); it('and if the call was successful, it should dispatch a ContactSuccessSubmit', () => { - const successAction = new DaffContactSuccessSubmit(); + const successAction = new DaffContactSubmitSuccess(); spyOn(daffContactDriver, 'send').and.returnValue(of('mystring')); actions$ = hot('--a', { a: forumSubmit }); @@ -58,7 +55,7 @@ describe('DaffContactEffects', () => { const error = [{ code: 'code', recoverable: false, message: 'Failed to submit' }]; const response = cold('#', {}, error[0]); spyOn(daffContactDriver, 'send').and.returnValue(response); - const failedAction = new DaffContactFailedSubmit(error); + const failedAction = new DaffContactSubmitFailure(error); actions$ = hot('--a', { a: forumSubmit }); expected = cold('--b', { b: failedAction }); @@ -71,7 +68,7 @@ describe('DaffContactEffects', () => { const forumSubmit = new DaffContactRetry(mockForm); it('and if the call was successful, it should dispatch a ContactSuccessSubmit', () => { - const successAction = new DaffContactSuccessSubmit(); + const successAction = new DaffContactSubmitSuccess(); spyOn(daffContactDriver, 'send').and.returnValue(of('mystring')); actions$ = hot('--a', { a: forumSubmit }); @@ -83,7 +80,7 @@ describe('DaffContactEffects', () => { const error = [{ code: 'code', recoverable: false, message: 'Failed to submit' }]; const response = cold('#', {}, error[0]); spyOn(daffContactDriver, 'send').and.returnValue(response); - const failedAction = new DaffContactFailedSubmit(error); + const failedAction = new DaffContactSubmitFailure(error); actions$ = hot('--a', { a: forumSubmit }); expected = cold('--b', { b: failedAction }); diff --git a/libs/contact/state/src/effects/contact.effects.ts b/libs/contact/state/src/effects/contact.effects.ts index ba917f1ccb..0d6ac3dae6 100644 --- a/libs/contact/state/src/effects/contact.effects.ts +++ b/libs/contact/state/src/effects/contact.effects.ts @@ -16,48 +16,47 @@ import { import { switchMap, map, - catchError, } from 'rxjs/operators'; import { DAFF_CONTACT_ERROR_MATCHER } from '@daffodil/contact'; import { DaffContactServiceInterface, DaffContactDriver, + DaffContactRequest, } from '@daffodil/contact/driver'; -import { DaffError } from '@daffodil/core'; +import { catchAndArrayifyErrors } from '@daffodil/core'; import { ErrorTransformer } from '@daffodil/core/state'; import { DaffContactActionTypes, DaffContactSubmit, DaffContactCancel, - DaffContactSuccessSubmit, - DaffContactFailedSubmit, + DaffContactSubmitSuccess, + DaffContactSubmitFailure, DaffContactRetry, } from '../actions/contact.actions'; @Injectable() -export class DaffContactEffects { +export class DaffContactEffects { constructor( private actions$: Actions, @Inject(DaffContactDriver) - private driver: DaffContactServiceInterface, + private driver: DaffContactServiceInterface, @Inject(DAFF_CONTACT_ERROR_MATCHER) private errorMatcher: ErrorTransformer, ) {} trySubmission$: Observable = createEffect(() => this.actions$.pipe( ofType( - DaffContactActionTypes.ContactSubmitAction, - DaffContactActionTypes.ContactRetryAction, - DaffContactActionTypes.ContactCancelAction, + DaffContactActionTypes.Submit, + DaffContactActionTypes.Retry, + DaffContactActionTypes.Cancel, ), switchMap( ( action: - | DaffContactSubmit - | DaffContactRetry - | DaffContactCancel, + | DaffContactSubmit + | DaffContactRetry, ) => { if (action instanceof DaffContactCancel) { return EMPTY; @@ -69,10 +68,10 @@ export class DaffContactEffects { ), ); - private submitContact(contact: T): Observable { + private submitContact(contact: DaffContactRequest): Observable { return this.driver.send(contact).pipe( - map((resp: V) => new DaffContactSuccessSubmit()), - catchError((error: DaffError) => of(new DaffContactFailedSubmit([this.errorMatcher(error)]))), + map(() => new DaffContactSubmitSuccess()), + catchAndArrayifyErrors((errors) => of(new DaffContactSubmitFailure(errors.map(this.errorMatcher)))), ); } } diff --git a/libs/contact/state/src/facades/contact.facade.spec.ts b/libs/contact/state/src/facades/contact.facade.spec.ts index 09b1b75745..04cf1f2e57 100644 --- a/libs/contact/state/src/facades/contact.facade.spec.ts +++ b/libs/contact/state/src/facades/contact.facade.spec.ts @@ -6,10 +6,10 @@ import { import { cold } from 'jasmine-marbles'; import { - reducer, - DaffContactSuccessSubmit, + daffContactStateReducer, + DaffContactSubmitSuccess, DaffContactSubmit, - DaffContactFailedSubmit, + DaffContactSubmitFailure, DaffContactStateRootSlice, DAFF_CONTACT_STORE_FEATURE_KEY, } from '@daffodil/contact/state'; @@ -24,7 +24,7 @@ describe('the DaffContactFacade', () => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({ - [DAFF_CONTACT_STORE_FEATURE_KEY]: reducer, + [DAFF_CONTACT_STORE_FEATURE_KEY]: daffContactStateReducer, }), ], providers: [DaffContactFacade], @@ -54,7 +54,7 @@ describe('the DaffContactFacade', () => { it('should return true after a successful submission', () => { const expected = cold('a', { a: true }); - store.dispatch(new DaffContactSuccessSubmit()); + store.dispatch(new DaffContactSubmitSuccess()); expect(facade.success$).toBeObservable(expected); }); }); @@ -82,7 +82,7 @@ describe('the DaffContactFacade', () => { it('should return an error when it fails', () => { const error = [{ code: 'code', message: 'Failed to submit' }]; const expected = cold('a', { a: error }); - store.dispatch(new DaffContactFailedSubmit(error)); + store.dispatch(new DaffContactSubmitFailure(error)); expect(facade.error$).toBeObservable(expected); }); }); diff --git a/libs/contact/state/src/public_api.ts b/libs/contact/state/src/public_api.ts index 86402ae788..02e6f56307 100644 --- a/libs/contact/state/src/public_api.ts +++ b/libs/contact/state/src/public_api.ts @@ -2,10 +2,10 @@ export { DaffContactActionTypes, DaffContactActions, DaffContactCancel, - DaffContactFailedSubmit, + DaffContactSubmitFailure, DaffContactReset, DaffContactRetry, - DaffContactSuccessSubmit, + DaffContactSubmitSuccess, DaffContactSubmit, } from './actions/contact.actions'; @@ -14,7 +14,8 @@ export { DaffContactFacade } from './facades/contact.facade'; export { DaffContactFacadeInterface } from './facades/contact-facade.interface'; export { DaffContactState, - reducer, + daffContactStateReducer, + daffContactReducerInitialState, DaffContactStateRootSlice, } from './reducers/contact.reducer'; export { DAFF_CONTACT_STORE_FEATURE_KEY } from './reducers/contact-store-feature-key'; diff --git a/libs/contact/state/src/reducers/contact.reducer.spec.ts b/libs/contact/state/src/reducers/contact.reducer.spec.ts index e56301da82..2949b6a477 100644 --- a/libs/contact/state/src/reducers/contact.reducer.spec.ts +++ b/libs/contact/state/src/reducers/contact.reducer.spec.ts @@ -2,94 +2,64 @@ import { DaffContactSubmit, DaffContactRetry, DaffContactCancel, - DaffContactFailedSubmit, - DaffContactSuccessSubmit, + DaffContactSubmitFailure, + DaffContactSubmitSuccess, DaffContactReset, } from '@daffodil/contact/state'; +import { DaffState } from '@daffodil/core/state'; import { DaffContactState, - reducer, + daffContactStateReducer as reducer, + daffContactReducerInitialState as initialState, } from './contact.reducer'; describe('the contact reducer', () => { it('should create an initial state', () => { - const expectedState: DaffContactState = { - errors: [], - loading: false, - success: false, - }; - const action = {}; - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action)).toEqual(initialState); }); it('should start loading when a submit action occurs', () => { - const expectedState: DaffContactState = { - errors: [], - loading: true, - success: false, - }; const payload = { email: '', firstName: '', lastName: '' }; const action = new DaffContactSubmit(payload); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action).daffState).toEqual(DaffState.Updating); }); it('should start loading when a retry action occurs', () => { - const expectedState: DaffContactState = { - errors: [], - loading: true, - success: false, - }; const payload = { email: '', firstName: '', lastName: '' }; const action = new DaffContactRetry(payload); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action).daffState).toEqual(DaffState.Updating); }); it('should stop loading when a cancel action occurs', () => { - const expectedState: DaffContactState = { - errors: [], - loading: false, - success: false, - }; const action = new DaffContactCancel(); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action).daffState).toEqual(DaffState.Stable); }); it('should return an error and stop loading if a failure action occurs', () => { - const error = [{ code: 'code', message: 'Failed to submit' }]; - const expectedState: DaffContactState = { - errors: error, - loading: false, - success: false, - }; - const action = new DaffContactFailedSubmit(error); + const error = { code: 'code', message: 'Failed to submit' }; + const action = new DaffContactSubmitFailure([error]); + const result = reducer(undefined, action); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(result.daffErrors).toContain(error); + expect(result.daffState).toContain(DaffState.Error); }); it('should set success to equal true and stop loading after a success action occurs', () => { - const expectedState: DaffContactState = { - errors: [], - loading: false, - success: true, - }; - const action = new DaffContactSuccessSubmit(); + const action = new DaffContactSubmitSuccess(); + const result = reducer(undefined, action); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(result.success).toBeTrue(); + expect(result.daffState).toContain(DaffState.Stable); }); it('it should reset to the intialState when a reset action occurs', () => { - const expectedState: DaffContactState = { - errors: [], - loading: false, - success: false, - }; const action = new DaffContactReset(); - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action)).toEqual(initialState); }); }); diff --git a/libs/contact/state/src/reducers/contact.reducer.ts b/libs/contact/state/src/reducers/contact.reducer.ts index 8b3391d638..43a7c36fde 100644 --- a/libs/contact/state/src/reducers/contact.reducer.ts +++ b/libs/contact/state/src/reducers/contact.reducer.ts @@ -1,4 +1,12 @@ -import { DaffStateError } from '@daffodil/core/state'; +import { ActionReducer } from '@ngrx/store'; + +import { + daffCompleteOperation, + daffOperationFailed, + daffOperationInitialState, + DaffOperationState, + daffStartMutation, +} from '@daffodil/core/state'; import { DAFF_CONTACT_STORE_FEATURE_KEY } from './contact-store-feature-key'; import { @@ -6,37 +14,38 @@ import { DaffContactActionTypes, } from '../actions/contact.actions'; -export interface DaffContactState { +export interface DaffContactState extends DaffOperationState { success: boolean; - loading: boolean; - errors: DaffStateError[]; } export interface DaffContactStateRootSlice { [DAFF_CONTACT_STORE_FEATURE_KEY]: DaffContactState; } -const initialState: DaffContactState = { +export const daffContactReducerInitialState: DaffContactState = { + ...daffOperationInitialState, success: false, - loading: false, - errors: [], }; -export function reducer(state: DaffContactState = initialState, - action: DaffContactActions){ - switch(action.type){ - case DaffContactActionTypes.ContactRetryAction: - case DaffContactActionTypes.ContactSubmitAction: - return { ...state, loading: true }; - case DaffContactActionTypes.ContactFailedSubmitAction: - return { ...state, loading: false, errors: action.payload }; - case DaffContactActionTypes.ContactSuccessSubmitAction: - return { ...state, success: true, loading: false }; - case DaffContactActionTypes.ContactCancelAction: - return { ...state, loading: false }; - case DaffContactActionTypes.ContactResetAction: - return { ...state, ... initialState }; +export const daffContactStateReducer: ActionReducer = (state: DaffContactState = daffContactReducerInitialState, action: DaffContactActions): DaffContactState => { + switch(action.type) { + case DaffContactActionTypes.Retry: + case DaffContactActionTypes.Submit: + return daffStartMutation(state); + + case DaffContactActionTypes.SubmitFailure: + return daffOperationFailed(action.payload, state); + + case DaffContactActionTypes.SubmitSuccess: + return { ...daffCompleteOperation(state), success: true }; + + case DaffContactActionTypes.Cancel: + return daffCompleteOperation(state); + + case DaffContactActionTypes.Reset: + return daffContactReducerInitialState; + default: return state; } -} +}; diff --git a/libs/contact/state/src/selectors/contact.selector.spec.ts b/libs/contact/state/src/selectors/contact.selector.spec.ts index 92db0a9662..7ec8234d4f 100644 --- a/libs/contact/state/src/selectors/contact.selector.spec.ts +++ b/libs/contact/state/src/selectors/contact.selector.spec.ts @@ -7,17 +7,14 @@ import { import { cold } from 'jasmine-marbles'; import { - reducer, + daffContactStateReducer, DaffContactState, DAFF_CONTACT_STORE_FEATURE_KEY, DaffContactStateRootSlice, } from '@daffodil/contact/state'; -import { - selectDaffContactLoading, - selectDaffContactSuccess, - selectDaffContactError, -} from './contact.selector'; +import { selectDaffContactSuccess } from './contact.selector'; +import { daffContactReducerInitialState } from '../reducers/contact.reducer'; describe('the contact selectors', () => { @@ -27,23 +24,14 @@ describe('the contact selectors', () => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({ - [DAFF_CONTACT_STORE_FEATURE_KEY]: reducer, + [DAFF_CONTACT_STORE_FEATURE_KEY]: daffContactStateReducer, }), ], }); - mockContactState = { loading: false, success: false, errors: []}; + mockContactState = daffContactReducerInitialState; store = TestBed.inject(Store); }); - describe('the selectDaffContactLoading', () => { - it('should select the loading property of the contact state', () =>{ - const selector = store.pipe(select(selectDaffContactLoading)); - const expected = cold('a', { a: mockContactState.loading }); - - expect(selector).toBeObservable(expected); - }); - }); - describe('the selectDaffContactSuccess', () => { it('should select the success property of the contact state', () =>{ const selector = store.pipe(select(selectDaffContactSuccess)); @@ -52,14 +40,4 @@ describe('the contact selectors', () => { expect(selector).toBeObservable(expected); }); }); - - describe('the selectDaffContactError', () => { - it('should select the error property of the contact state', () =>{ - const selector = store.pipe(select(selectDaffContactError)); - const expected = cold('a', { a: mockContactState.errors }); - - expect(selector).toBeObservable(expected); - }); - }); - }); diff --git a/libs/contact/state/src/selectors/contact.selector.ts b/libs/contact/state/src/selectors/contact.selector.ts index 98940fc9b2..6446d94779 100644 --- a/libs/contact/state/src/selectors/contact.selector.ts +++ b/libs/contact/state/src/selectors/contact.selector.ts @@ -2,10 +2,9 @@ import { MemoizedSelector, createFeatureSelector, createSelector, - DefaultProjectorFn, } from '@ngrx/store'; -import { DaffStateError } from '@daffodil/core/state'; +import { daffOperationStateSelectorFactory } from '@daffodil/core/state'; import { DAFF_CONTACT_STORE_FEATURE_KEY } from '../reducers/contact-store-feature-key'; import { @@ -16,15 +15,11 @@ import { export const selectContactFeatureState: MemoizedSelector = createFeatureSelector(DAFF_CONTACT_STORE_FEATURE_KEY); -export const selectDaffContactLoading = createSelector( - selectContactFeatureState, (state: DaffContactState) => state.loading, -); +export const { + selectLoading: selectDaffContactLoading, + selectErrors: selectDaffContactError, +} = daffOperationStateSelectorFactory(selectContactFeatureState); export const selectDaffContactSuccess = createSelector( selectContactFeatureState, (state: DaffContactState) => state.success, ); - -export const selectDaffContactError: MemoizedSelector> -= createSelector( - selectContactFeatureState, (state: DaffContactState) => state.errors, -); diff --git a/libs/contact/state/testing/ng-package.prod.json b/libs/contact/state/testing/ng-package.prod.json deleted file mode 100644 index d6b5de75d0..0000000000 --- a/libs/contact/state/testing/ng-package.prod.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/contact/state/testing", - "lib": { - "entryFile": "src/index.ts" - } -} \ No newline at end of file diff --git a/libs/driver/hubspot/ng-package.json b/libs/driver/hubspot/ng-package.json index 22e4fdefa6..9e480669ca 100644 --- a/libs/driver/hubspot/ng-package.json +++ b/libs/driver/hubspot/ng-package.json @@ -1,5 +1,5 @@ { - "$schema": "../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", "lib": { "entryFile": "src/index.ts" } diff --git a/libs/driver/hubspot/src/hubspot-forms.provider.spec.ts b/libs/driver/hubspot/src/hubspot-forms.provider.spec.ts index 42fe57727a..e4ac548679 100644 --- a/libs/driver/hubspot/src/hubspot-forms.provider.spec.ts +++ b/libs/driver/hubspot/src/hubspot-forms.provider.spec.ts @@ -10,12 +10,8 @@ import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { - DaffHubspotFormsService, - daffHubspotFormsServiceFactory, -} from '@daffodil/driver/hubspot'; - - +import { daffHubspotFormsServiceFactory } from './hubspot-forms.provider'; +import { DaffHubspotFormsService } from './hubspot-forms.service'; describe('DaffHubspotForms Factory Provider', () => { let hubspotService: DaffHubspotFormsService; diff --git a/libs/driver/hubspot/src/hubspot-forms.provider.ts b/libs/driver/hubspot/src/hubspot-forms.provider.ts index 2c25047986..84d8f96c04 100644 --- a/libs/driver/hubspot/src/hubspot-forms.provider.ts +++ b/libs/driver/hubspot/src/hubspot-forms.provider.ts @@ -4,12 +4,15 @@ import { Router } from '@angular/router'; import { DaffHubspotFormsService } from './hubspot-forms.service'; import { DaffHubspotConfig } from './models/config'; +import { DaffHubspotFormsInterface } from './models/forms'; - +/** + * Factory for {@link DaffHubspotFormsService}. + */ export const daffHubspotFormsServiceFactory = ( http: HttpClient, document: Document, router: Router, title: Title, config: DaffHubspotConfig, -) => new DaffHubspotFormsService(http, document, router, title, config); +): DaffHubspotFormsInterface => new DaffHubspotFormsService(http, document, router, title, config); diff --git a/libs/driver/hubspot/src/hubspot-forms.service.ts b/libs/driver/hubspot/src/hubspot-forms.service.ts index cd1f079c36..c65d772b24 100644 --- a/libs/driver/hubspot/src/hubspot-forms.service.ts +++ b/libs/driver/hubspot/src/hubspot-forms.service.ts @@ -4,12 +4,15 @@ import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { DaffHubspotConfig } from './models/config'; +import { DaffHubspotFormsInterface } from './models/forms'; import { DaffHubspotRequest } from './models/hubspot-request'; import { HubspotResponse } from './models/hubspot-response'; import { jsonBuilder } from './transformers/json-builder'; -export class DaffHubspotFormsService { - +/** + * Service for interacting with Hubspot Forms API. + */ +export class DaffHubspotFormsService implements DaffHubspotFormsInterface { constructor( private http: HttpClient, private document: Document, diff --git a/libs/driver/hubspot/src/index.ts b/libs/driver/hubspot/src/index.ts index c5ad30783d..4aaf8f92ed 100644 --- a/libs/driver/hubspot/src/index.ts +++ b/libs/driver/hubspot/src/index.ts @@ -1,4 +1 @@ -export { DaffHubspotFormsService } from './hubspot-forms.service'; -export { DaffHubspotConfig } from './models/config'; -export { DaffHubspotRequest } from './models/hubspot-request'; -export { daffHubspotFormsServiceFactory } from './hubspot-forms.provider'; +export * from './public_api'; diff --git a/libs/driver/hubspot/src/models/forms.ts b/libs/driver/hubspot/src/models/forms.ts new file mode 100644 index 0000000000..94aa265a24 --- /dev/null +++ b/libs/driver/hubspot/src/models/forms.ts @@ -0,0 +1,10 @@ +import { Observable } from 'rxjs'; + +import { HubspotResponse } from './hubspot-response'; + +/** + * Interface for interacting with Hubspot Forms API. + */ +export interface DaffHubspotFormsInterface { + submit(payload: Record): Observable; +} diff --git a/libs/driver/hubspot/src/public_api.ts b/libs/driver/hubspot/src/public_api.ts new file mode 100644 index 0000000000..007a13ab31 --- /dev/null +++ b/libs/driver/hubspot/src/public_api.ts @@ -0,0 +1,9 @@ +export { DaffHubspotFormsService } from './hubspot-forms.service'; +export { DaffHubspotConfig } from './models/config'; +export { DaffHubspotRequest } from './models/hubspot-request'; +export { + HubspotResponse, + HubspotError, +} from './models/hubspot-response'; +export { DaffHubspotFormsInterface } from './models/forms'; +export { daffHubspotFormsServiceFactory } from './hubspot-forms.provider'; diff --git a/libs/driver/hubspot/testing/ng-package.json b/libs/driver/hubspot/testing/ng-package.json new file mode 100644 index 0000000000..7dcb29e536 --- /dev/null +++ b/libs/driver/hubspot/testing/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.spec.ts b/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.spec.ts new file mode 100644 index 0000000000..2e9b25ef08 --- /dev/null +++ b/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.spec.ts @@ -0,0 +1,39 @@ +import { TestBed } from '@angular/core/testing'; + +import { HubspotResponse } from '@daffodil/driver/hubspot/models/hubspot-response'; + +import { DaffHubspotResponseFactory } from './hubspot-response.factory'; + +describe('@daffodil/driver/hubspot/testing | DaffHubspotResponseFactory', () => { + let hubspotResponseFactory: DaffHubspotResponseFactory; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DaffHubspotResponseFactory], + }); + + hubspotResponseFactory = TestBed.inject(DaffHubspotResponseFactory); + }); + + it('should be created', () => { + expect(hubspotResponseFactory).toBeTruthy(); + }); + + describe('create', () => { + let result: HubspotResponse; + + beforeEach(() => { + result = hubspotResponseFactory.create(); + }); + + it('should return', () => { + expect(result).toBeDefined(); + }); + + it('should define all the required fields', () => { + expect(result.redirectUri).toBeDefined(); + expect(result.inlineMessage).toBeDefined(); + expect(result.errors).toBeDefined(); + }); + }); +}); diff --git a/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.ts b/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.ts new file mode 100644 index 0000000000..072eb7de3f --- /dev/null +++ b/libs/driver/hubspot/testing/src/factories/hubspot-response.factory.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { faker } from '@faker-js/faker'; + +import { DaffModelFactory } from '@daffodil/core/testing'; +import { + HubspotError, + HubspotResponse, +} from '@daffodil/driver/hubspot'; + +const MockHubspotError = (): HubspotError => ({ + message: faker.random.words(5), + errorType: faker.random.word(), +}); + +export class MockHubspotResponse implements HubspotResponse { + redirectUri? = faker.internet.url(); + inlineMessage = faker.random.words(5); + errors: HubspotError[] = Array(faker.datatype.number({ min: 1, max: 5 })).fill(MockHubspotError); +} + +/** + * Model factory for {@link MockHubspotResponse}s. + * + * Should be used to create {@link MockHubspotResponse}s for testing purposes. + */ +@Injectable({ + providedIn: 'root', +}) +export class DaffHubspotResponseFactory extends DaffModelFactory{ + constructor() { + super(MockHubspotResponse); + } +} diff --git a/libs/driver/hubspot/testing/src/factories/public_api.ts b/libs/driver/hubspot/testing/src/factories/public_api.ts new file mode 100644 index 0000000000..bfaf6b2f61 --- /dev/null +++ b/libs/driver/hubspot/testing/src/factories/public_api.ts @@ -0,0 +1 @@ +export * from './hubspot-response.factory'; diff --git a/libs/driver/hubspot/testing/src/index.ts b/libs/driver/hubspot/testing/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/driver/hubspot/testing/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/driver/hubspot/testing/src/public_api.ts b/libs/driver/hubspot/testing/src/public_api.ts new file mode 100644 index 0000000000..b834ec22bb --- /dev/null +++ b/libs/driver/hubspot/testing/src/public_api.ts @@ -0,0 +1 @@ +export * from './factories/public_api'; diff --git a/libs/driver/tsconfig.json b/libs/driver/tsconfig.json index 014385a89c..f265882051 100644 --- a/libs/driver/tsconfig.json +++ b/libs/driver/tsconfig.json @@ -12,6 +12,9 @@ "@daffodil/driver/hubspot": [ "libs/driver/hubspot/src" ], + "@daffodil/driver/hubspot/testing": [ + "libs/driver/hubspot/testing/src" + ], "@daffodil/driver/magento": [ "libs/driver/magento/src" ], diff --git a/libs/newsletter/driver/hubspot/src/newsletter.service.spec.ts b/libs/newsletter/driver/hubspot/src/newsletter.service.spec.ts index 9cdcf8cb5d..6ac06df071 100644 --- a/libs/newsletter/driver/hubspot/src/newsletter.service.spec.ts +++ b/libs/newsletter/driver/hubspot/src/newsletter.service.spec.ts @@ -1,42 +1,56 @@ import { TestBed } from '@angular/core/testing'; -import { - cold, - hot, -} from 'jasmine-marbles'; +import { hot } from 'jasmine-marbles'; import { Observable } from 'rxjs'; +import { HubspotResponse } from '@daffodil/driver/hubspot'; +import { DaffHubspotResponseFactory } from '@daffodil/driver/hubspot/testing'; +import { DaffNewsletterSubmission } from '@daffodil/newsletter'; +import { DaffNewsletterHubSpotDriverModule } from '@daffodil/newsletter/driver/hubspot'; + import { DaffNewsletterHubspotService } from './newsletter.service'; import { DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN } from './token/hubspot-forms.token'; -describe('DaffNewsletterHubspotService', () => { - let newsletterService; +describe('newsletterHubspotService', () => { + let newsletterHubspotService: DaffNewsletterHubspotService; + const responseFactory: DaffHubspotResponseFactory = new DaffHubspotResponseFactory(); + + const sampleResponse: HubspotResponse = responseFactory.create(); beforeEach(() => { TestBed.configureTestingModule({ + imports: [ + DaffNewsletterHubSpotDriverModule.forRoot({ + portalId: '123123', + guid: '123123', + }), + ], providers: [ DaffNewsletterHubspotService, { provide: DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN, useValue: { - submit: (): Observable => hot('--a', { a: { test: '123' }}), + submit: (): Observable => hot('--a', { a: sampleResponse }), }, }, ], }); - newsletterService = TestBed.inject(DaffNewsletterHubspotService); + newsletterHubspotService = TestBed.inject(DaffNewsletterHubspotService); }); - it('should be created', () => { - expect(newsletterService).toBeTruthy(); + it('should take a DaffNewsletterSubmission string convert a HubspotResponse to a DaffNewsletterResponse', () => { + const newsletterSubmission: DaffNewsletterSubmission = 'test@email.com'; + + newsletterHubspotService.send(newsletterSubmission).subscribe((resp) => { + expect(resp).toEqual({ message: sampleResponse.inlineMessage }); + }); }); - describe('when sending', () => { - it('should return an observable of HubspotResponse', () => { - const payload = { email: 'email@email.edu' }; - const expected = cold('--b', { b: { test: '123' }}); + it('should take a DaffNewsletterSubmission convert a HubspotResponse to a DaffNewsletterResponse', () => { + const newsletterSubmission: DaffNewsletterSubmission = 'test@email.com'; - expect(newsletterService.send(payload)).toBeObservable(expected); + newsletterHubspotService.send(newsletterSubmission).subscribe((resp) => { + expect(resp).toEqual({ message: sampleResponse.inlineMessage }); }); }); }); diff --git a/libs/newsletter/driver/hubspot/src/newsletter.service.ts b/libs/newsletter/driver/hubspot/src/newsletter.service.ts index 0e180313f4..78974c584e 100644 --- a/libs/newsletter/driver/hubspot/src/newsletter.service.ts +++ b/libs/newsletter/driver/hubspot/src/newsletter.service.ts @@ -3,19 +3,33 @@ import { Injectable, } from '@angular/core'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; -import { DaffHubspotFormsService } from '@daffodil/driver/hubspot'; -import { DaffNewsletterUnion } from '@daffodil/newsletter'; +import { DaffHubspotFormsInterface } from '@daffodil/driver/hubspot'; +import { HubspotResponse } from '@daffodil/driver/hubspot/models/hubspot-response'; +import { + DaffNewsletterResponse, + DaffNewsletterSubmission, +} from '@daffodil/newsletter'; import { DaffNewsletterServiceInterface } from '@daffodil/newsletter/driver'; import { DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN } from './token/hubspot-forms.token'; +/** + * @inheritdoc + */ @Injectable() -export class DaffNewsletterHubspotService implements DaffNewsletterServiceInterface { +export class DaffNewsletterHubspotService implements DaffNewsletterServiceInterface { - constructor(@Inject(DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN) private hubspotService: DaffHubspotFormsService) {} + constructor(@Inject(DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN) private hubspotService: DaffHubspotFormsInterface) {} - send(payload: DaffNewsletterUnion): Observable { - return this.hubspotService.submit(payload); + send(payload: DaffNewsletterSubmission): Observable { + return this.hubspotService.submit({ + email: payload, + }).pipe( + map((response: HubspotResponse): DaffNewsletterResponse => ({ + message: response.inlineMessage, + }), + )); } } diff --git a/libs/newsletter/driver/hubspot/src/token/hubspot-forms.token.ts b/libs/newsletter/driver/hubspot/src/token/hubspot-forms.token.ts index 4c2d4629f0..4a82d99a69 100644 --- a/libs/newsletter/driver/hubspot/src/token/hubspot-forms.token.ts +++ b/libs/newsletter/driver/hubspot/src/token/hubspot-forms.token.ts @@ -8,13 +8,17 @@ import { Title } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { - DaffHubspotFormsService, daffHubspotFormsServiceFactory, + DaffHubspotFormsInterface, } from '@daffodil/driver/hubspot'; import { DaffNewsletterConfigToken } from '../config/newsletter-config.interface'; -export const DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN = new InjectionToken('DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN', +/** + * The InjectionToken that holds the Hubspot Forms Service + * used by the HubspotDriver to send submissions to Hubspot. + */ +export const DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN = new InjectionToken('DAFF_NEWSLETTER_HUBSPOT_FORMS_TOKEN', { providedIn: 'root', factory: () => daffHubspotFormsServiceFactory( inject(HttpClient), diff --git a/libs/newsletter/driver/in-memory/src/backend/newsletter.service.spec.ts b/libs/newsletter/driver/in-memory/src/backend/newsletter.service.spec.ts index 7b6c7964b1..5678598ed5 100644 --- a/libs/newsletter/driver/in-memory/src/backend/newsletter.service.spec.ts +++ b/libs/newsletter/driver/in-memory/src/backend/newsletter.service.spec.ts @@ -1,29 +1,36 @@ import { TestBed } from '@angular/core/testing'; - -import { DaffNewsletterSubmission } from '@daffodil/newsletter'; +import { STATUS } from 'angular-in-memory-web-api'; import { DaffInMemoryBackendNewsletterService } from './newsletter.service'; -describe('DaffNewsletterInMemoryBackend', () => { - let newsletterTestingService; +describe('@daffodil/newsletter/driver/in-memory | DaffInMemoryBackendNewsletterService', () => { + let service: DaffInMemoryBackendNewsletterService; + let result; + let reqInfoStub; beforeEach(() => { TestBed.configureTestingModule({ providers: [DaffInMemoryBackendNewsletterService], }); - newsletterTestingService = TestBed.inject(DaffInMemoryBackendNewsletterService); + service = TestBed.inject(DaffInMemoryBackendNewsletterService); + + reqInfoStub = { + req: {}, + utils: { + createResponse$: f => f(), + getJsonBody: req => req.body, + }, + }; }); it('should be created', () => { - expect(newsletterTestingService).toBeTruthy(); + expect(service).toBeTruthy(); }); describe('after intializiaton', () => { - let result; - beforeEach(() => { - result = newsletterTestingService.createDb(); + result = service.createDb(); }); it('should have any empty database', () => { @@ -32,19 +39,24 @@ describe('DaffNewsletterInMemoryBackend', () => { }); it('should validate that a submission is not empty', () => { - const newsletterSubmission: DaffNewsletterSubmission = undefined; - expect(newsletterTestingService.post(newsletterSubmission)).toEqual(Error('Payload is undefined')); + reqInfoStub.req.body = undefined; + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.BAD_REQUEST); + expect(result.statusText).toEqual('Payload is undefined'); }); it('should validate that a submission already exists', () => { - - const newsletterSubmission: DaffNewsletterSubmission = { email: 'test@test.com' }; - newsletterTestingService.post(newsletterSubmission); - expect(newsletterTestingService.post(newsletterSubmission)).toEqual(Error('Already contains submission')); + reqInfoStub.req.body = { email: 'test@test.com' }; + service.post(reqInfoStub); + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.BAD_REQUEST); + expect(result.statusText).toEqual('Already contains submission'); }); it('should not throw an error if it is in the 0th position', () => { - const newsletterSubmission: DaffNewsletterSubmission = { email: 'test2@test.com' }; - expect(newsletterTestingService.post(newsletterSubmission)).toEqual(newsletterSubmission); + reqInfoStub.req.body = { email: 'test2@test.com' }; + result = service.post(reqInfoStub); + expect(result.status).toEqual(STATUS.OK); + expect(result.body).toBeTrue(); }); }); diff --git a/libs/newsletter/driver/in-memory/src/backend/newsletter.service.ts b/libs/newsletter/driver/in-memory/src/backend/newsletter.service.ts index ead893575b..0239befae4 100644 --- a/libs/newsletter/driver/in-memory/src/backend/newsletter.service.ts +++ b/libs/newsletter/driver/in-memory/src/backend/newsletter.service.ts @@ -3,20 +3,27 @@ import { InMemoryDbService, RequestInfoUtilities, ParsedRequestUrl, + RequestInfo, + STATUS, } from 'angular-in-memory-web-api'; import { DaffInMemorySingleRouteableBackend } from '@daffodil/driver/in-memory'; -import { DaffNewsletterUnion } from '@daffodil/newsletter'; +import { DaffNewsletterSubmission } from '@daffodil/newsletter'; import { DAFF_NEWSLETTER_IN_MEMORY_COLLECTION_NAME } from '../collection-name.const'; +/** + * An in-memory service that handles newsletter requests. + * + * @inheritdoc + */ @Injectable({ providedIn: 'root', }) export class DaffInMemoryBackendNewsletterService implements InMemoryDbService, DaffInMemorySingleRouteableBackend { readonly collectionName = DAFF_NEWSLETTER_IN_MEMORY_COLLECTION_NAME; - newsletters: DaffNewsletterUnion[] = []; + newsletters: Array = []; parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl { return utils.parseRequestUrl(url); @@ -27,16 +34,30 @@ export class DaffInMemoryBackendNewsletterService implements InMemoryDbService, newsletters: this.newsletters, }; } - //validate that its not empty - //validate that it doesn't already exist - post(reqInfo: any) { - if (reqInfo === undefined) { - return Error('Payload is undefined'); - } else if (this.newsletters.indexOf(reqInfo) > -1) { - return Error('Already contains submission'); - } else { - this.newsletters.push(reqInfo); - return reqInfo; - } + + post(reqInfo: RequestInfo) { + const body = reqInfo.utils.getJsonBody(reqInfo.req); + + return reqInfo.utils.createResponse$(() => { + // validate that its not empty + if (body === undefined) { + return { + status: STATUS.BAD_REQUEST, + statusText: 'Payload is undefined', + }; + // validate that it doesn't already exist + } else if (this.newsletters.includes(body)) { + return { + status: STATUS.BAD_REQUEST, + statusText: 'Already contains submission', + }; + } else { + this.newsletters.push(body); + return { + status: STATUS.OK, + body: true, + }; + } + }); } } diff --git a/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.spec.ts b/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.spec.ts index 9dfd5353fe..dc582ce43e 100644 --- a/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.spec.ts +++ b/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.spec.ts @@ -46,7 +46,7 @@ describe('@daffodil/newsletter/driver/in-memory | NewsletterService', () => { describe('when sending', () => { it('should send a POST request', () => { - const newsletterSubmission: DaffNewsletterSubmission = { email: 'test@email.com' }; + const newsletterSubmission: DaffNewsletterSubmission = 'test@email.com'; newsletterService.send(newsletterSubmission).subscribe(); @@ -56,7 +56,7 @@ describe('@daffodil/newsletter/driver/in-memory | NewsletterService', () => { }); it('should send a submission', () => { - const newsletterSubmission: DaffNewsletterSubmission = { email: 'test@email.com' }; + const newsletterSubmission: DaffNewsletterSubmission = 'test@email.com'; newsletterService.send(newsletterSubmission).subscribe(); @@ -66,17 +66,5 @@ describe('@daffodil/newsletter/driver/in-memory | NewsletterService', () => { }); - - it('should send a submission that extends the DaffNewsletterSubmission', () => { - const newsletterSubmission = { email: 'test@email.com', name: 'James Arnold' }; - - newsletterService.send(newsletterSubmission).subscribe(); - - const req = httpMock.expectOne(`${newsletterService['url']}`); - expect(req.request.body).toBe(newsletterSubmission); - - }); - }); - }); diff --git a/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.ts b/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.ts index 6402ea93ef..37e4e07f51 100644 --- a/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.ts +++ b/libs/newsletter/driver/in-memory/src/drivers/newsletter.service.ts @@ -4,7 +4,10 @@ import { InMemoryBackendConfig } from 'angular-in-memory-web-api'; import { Observable } from 'rxjs'; import { DaffInMemoryDriverBase } from '@daffodil/driver/in-memory'; -import { DaffNewsletterUnion } from '@daffodil/newsletter'; +import { + DaffNewsletterSubmission, + DaffNewsletterResponse, +} from '@daffodil/newsletter'; import { DaffNewsletterServiceInterface } from '@daffodil/newsletter/driver'; import { DAFF_NEWSLETTER_IN_MEMORY_COLLECTION_NAME } from '../collection-name.const'; @@ -17,7 +20,7 @@ import { DAFF_NEWSLETTER_IN_MEMORY_COLLECTION_NAME } from '../collection-name.co @Injectable({ providedIn: 'root', }) -export class DaffInMemoryNewsletterService extends DaffInMemoryDriverBase implements DaffNewsletterServiceInterface{ +export class DaffInMemoryNewsletterService extends DaffInMemoryDriverBase implements DaffNewsletterServiceInterface { constructor( private http: HttpClient, config: InMemoryBackendConfig, @@ -28,11 +31,10 @@ export class DaffInMemoryNewsletterService extends DaffInMemoryDriverBase implem /** * Sends your newsletter submission data. * - * @param payload DaffNewsletterUnion - * @returns An Observable of DaffNewsletterUnion + * @param payload DaffNewsletterSubmission + * @returns An Observable of DaffNewsletterResponse */ - send(payload: DaffNewsletterUnion): Observable { - return this.http.post(this.url, payload); + send(payload: DaffNewsletterSubmission): Observable { + return this.http.post(this.url, payload); } - } diff --git a/libs/newsletter/driver/in-memory/src/public_api.ts b/libs/newsletter/driver/in-memory/src/public_api.ts index 4a72fdfdb4..d395346524 100644 --- a/libs/newsletter/driver/in-memory/src/public_api.ts +++ b/libs/newsletter/driver/in-memory/src/public_api.ts @@ -1,2 +1,3 @@ export * from './drivers/public_api'; export * from './backend/public_api'; +export * from './collection-name.const'; diff --git a/libs/newsletter/driver/src/interfaces/newsletter-service.interface.ts b/libs/newsletter/driver/src/interfaces/newsletter-service.interface.ts index ecad6460fd..eb5cdbc758 100644 --- a/libs/newsletter/driver/src/interfaces/newsletter-service.interface.ts +++ b/libs/newsletter/driver/src/interfaces/newsletter-service.interface.ts @@ -1,10 +1,26 @@ import { InjectionToken } from '@angular/core'; import { Observable } from 'rxjs'; -import { DaffNewsletterSubmission } from '@daffodil/newsletter'; +import { + DaffNewsletterSubmission, + DaffNewsletterResponse, +} from '@daffodil/newsletter'; -export const DaffNewsletterDriver = new InjectionToken>('DaffNewsletterDriver'); +/** + * The token for the newsletter driver. + */ +export const DaffNewsletterDriver: InjectionToken = new InjectionToken('DaffNewsletterDriver'); -export interface DaffNewsletterServiceInterface { - send(email: T): Observable; +/** + * The interface responsible for sending newsletter submissions. + */ +export interface DaffNewsletterServiceInterface { + /** + * Sends a newsletter submission and returns a response. + * + * @param email The email to store for the newsletter subscription. + * + * @returns The response from the newsletter submission. + */ + send(email: DaffNewsletterSubmission): Observable; } diff --git a/libs/newsletter/driver/testing/src/drivers/newsletter.service.ts b/libs/newsletter/driver/testing/src/drivers/newsletter.service.ts index 3e2c2912b7..58964fac64 100644 --- a/libs/newsletter/driver/testing/src/drivers/newsletter.service.ts +++ b/libs/newsletter/driver/testing/src/drivers/newsletter.service.ts @@ -5,15 +5,20 @@ import { } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { DaffNewsletterUnion } from '@daffodil/newsletter'; +import { + DaffNewsletterResponse, + DaffNewsletterSubmission, +} from '@daffodil/newsletter'; import { DaffNewsletterServiceInterface } from '@daffodil/newsletter/driver'; +/** + * @inheritdoc + */ @Injectable({ providedIn: 'root', }) - -export class DaffTestingNewsletterService implements DaffNewsletterServiceInterface{ - send(payload: DaffNewsletterUnion): Observable{ - return of('Success').pipe(delay(10)); +export class DaffTestingNewsletterService implements DaffNewsletterServiceInterface { + send(payload: DaffNewsletterSubmission): Observable{ + return of({ message: 'Success' }).pipe(delay(10)); } } diff --git a/libs/newsletter/integration-tests/drivers/hubspot.spec.ts b/libs/newsletter/integration-tests/drivers/hubspot.spec.ts index 4a54daea46..b3317eae1d 100644 --- a/libs/newsletter/integration-tests/drivers/hubspot.spec.ts +++ b/libs/newsletter/integration-tests/drivers/hubspot.spec.ts @@ -17,7 +17,7 @@ import { import { DaffNewsletterHubSpotDriverModule } from '@daffodil/newsletter/driver/hubspot'; describe('DaffNewsletterHubspotDriver', () => { - let newsletterService: DaffNewsletterServiceInterface; + let newsletterService: DaffNewsletterServiceInterface; let httpMock: HttpTestingController; beforeEach(() => { @@ -40,14 +40,12 @@ describe('DaffNewsletterHubspotDriver', () => { }); it('should allow a developer to configure and send newsletter subscription requests to the HubspotForms API', () => { - const newsletterSubmission = { email: 'test@email.com' }; - newsletterService.send(newsletterSubmission).subscribe((resp) => { - expect(resp).toEqual(newsletterSubmission); - }); + const newsletterSubmission: DaffNewsletterSubmission = 'test@email.com'; + newsletterService.send(newsletterSubmission).subscribe(); const req = httpMock.expectOne('https://api.hsforms.com/submissions/v3/integration/submit/123123/123123'); expect(req.request.body).toEqual(jasmine.objectContaining({ fields: [Object({ name: 'email', value: 'test@email.com' })], - context: Object({ hutk: null, pageUri: '/', pageName: jasmine.any(String) }), + context: jasmine.objectContaining({ pageUri: '/', pageName: jasmine.any(String) }), })); req.flush(newsletterSubmission); httpMock.verify(); diff --git a/libs/newsletter/src/models/newsletter-response.type.ts b/libs/newsletter/src/models/newsletter-response.type.ts new file mode 100644 index 0000000000..bcc4f1e552 --- /dev/null +++ b/libs/newsletter/src/models/newsletter-response.type.ts @@ -0,0 +1,6 @@ +/** + * A type for the response of a newsletter subscription. + */ +export interface DaffNewsletterResponse { + message: string; +} diff --git a/libs/newsletter/src/models/newsletter-submission.type.ts b/libs/newsletter/src/models/newsletter-submission.type.ts new file mode 100644 index 0000000000..8a419833cf --- /dev/null +++ b/libs/newsletter/src/models/newsletter-submission.type.ts @@ -0,0 +1,5 @@ +/** + * The submission for subscribing to a newsletter. + * Typically an email address. + */ +export type DaffNewsletterSubmission = string; diff --git a/libs/newsletter/src/models/newsletter-union.ts b/libs/newsletter/src/models/newsletter-union.ts deleted file mode 100644 index b153d07db9..0000000000 --- a/libs/newsletter/src/models/newsletter-union.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { DaffNewsletterSubmission } from './newsletter.model'; - -export interface DaffNewsletterUnion extends DaffNewsletterSubmission { - [x: string]: any; -} diff --git a/libs/newsletter/src/models/newsletter.model.ts b/libs/newsletter/src/models/newsletter.model.ts deleted file mode 100644 index 7b7934d5c8..0000000000 --- a/libs/newsletter/src/models/newsletter.model.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DaffNewsletterSubmission { - email: string; -} diff --git a/libs/newsletter/src/public_api.ts b/libs/newsletter/src/public_api.ts index 9bc81251df..2fd22885d9 100644 --- a/libs/newsletter/src/public_api.ts +++ b/libs/newsletter/src/public_api.ts @@ -1,2 +1,2 @@ -export { DaffNewsletterSubmission } from './models/newsletter.model'; -export { DaffNewsletterUnion } from './models/newsletter-union'; +export { DaffNewsletterSubmission } from './models/newsletter-submission.type'; +export { DaffNewsletterResponse } from './models/newsletter-response.type'; diff --git a/libs/newsletter/state/src/actions/newsletter.actions.ts b/libs/newsletter/state/src/actions/newsletter.actions.ts index e703d73de7..edfbc9260d 100644 --- a/libs/newsletter/state/src/actions/newsletter.actions.ts +++ b/libs/newsletter/state/src/actions/newsletter.actions.ts @@ -1,48 +1,78 @@ import { Action } from '@ngrx/store'; -import { DaffStateError } from '@daffodil/core/state'; +import { + DaffFailureAction, + DaffStateError, +} from '@daffodil/core/state'; import { DaffNewsletterSubmission } from '@daffodil/newsletter'; export enum DaffNewsletterActionTypes { - NewsletterSubscribeAction = '[@daffodil/newsletter] Newsletter Subscribe Action', - NewsletterCancelAction = '[@daffodil/newsletter] Newsletter Cancel Action', - NewsletterSuccessSubscribeAction = '[@daffodil/newsletter] Succeeded on Newsletter Subscribe Action', - NewsletterFailedSubscribeAction = '[@daffodil/newsletter] Failed on Newsletter Subscribe Action', - NewsletterRetry = '[@daffodil/newsletter] Retrying submission', - NewsletterReset = '[@daffodil/newsletter] Reset Newsletter' + Subscribe = '[@daffodil/newsletter] Subscribe', + SubscribeSuccess = '[@daffodil/newsletter] Subscribe Success', + SubscribeFailure = '[@daffodil/newsletter] Subscribe Failure', + Cancel = '[@daffodil/newsletter] Cancel', + Retry = '[@daffodil/newsletter] Retrying Submission', + Reset = '[@daffodil/newsletter] Reset' } -export class DaffNewsletterSubscribe implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterSubscribeAction; +/** + * An action triggered upon subscribing to a newsletter. + * + * @param payload - a newsletter submission payload + */ +export class DaffNewsletterSubscribe implements Action { + readonly type = DaffNewsletterActionTypes.Subscribe; - constructor(public payload: T) { } + constructor(public payload: DaffNewsletterSubmission) { } } -export class DaffNewsletterRetry implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterRetry; - constructor(public payload: T) { } -} +/** + * An action triggered upon failure of a newsletter subscription request. + * + * @param payload - an array of errors + */ +export class DaffNewsletterSubscribeFailure implements DaffFailureAction { + readonly type = DaffNewsletterActionTypes.SubscribeFailure; -export class DaffNewsletterCancel implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterCancelAction; + constructor(public payload: Array) {} +} +/** + * An action triggered upon success of a newsletter subscription request. + */ +export class DaffNewsletterSubscribeSuccess implements Action { + readonly type = DaffNewsletterActionTypes.SubscribeSuccess; } -export class DaffNewsletterFailedSubscribe implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterFailedSubscribeAction; - constructor(public payload: DaffStateError) { } +/** + * An action triggered upon attempting to retry subscribing to a newsletter. + * + * @param payload - a newsletter submission payload + */ +export class DaffNewsletterRetry implements Action { + readonly type = DaffNewsletterActionTypes.Retry; + + constructor(public payload: DaffNewsletterSubmission) { } } -export class DaffNewsletterSuccessSubscribe implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterSuccessSubscribeAction; + +/** + * An action triggered upon cancelling a newsletter subscription request. + */ +export class DaffNewsletterCancel implements Action { + readonly type = DaffNewsletterActionTypes.Cancel; } + +/** + * An action triggered upon resetting of a newsletter subscription. + */ export class DaffNewsletterReset implements Action { - readonly type = DaffNewsletterActionTypes.NewsletterReset; + readonly type = DaffNewsletterActionTypes.Reset; } -export type DaffNewsletterActions = - DaffNewsletterSubscribe | - DaffNewsletterSuccessSubscribe | - DaffNewsletterFailedSubscribe | +export type DaffNewsletterActions = + DaffNewsletterSubscribe | + DaffNewsletterSubscribeSuccess | + DaffNewsletterSubscribeFailure | DaffNewsletterReset | - DaffNewsletterRetry | + DaffNewsletterRetry | DaffNewsletterCancel; diff --git a/libs/newsletter/state/src/effects/newsletter.effects.spec.ts b/libs/newsletter/state/src/effects/newsletter.effects.spec.ts index aefa40c865..a9ecc991d7 100644 --- a/libs/newsletter/state/src/effects/newsletter.effects.spec.ts +++ b/libs/newsletter/state/src/effects/newsletter.effects.spec.ts @@ -9,7 +9,6 @@ import { of, } from 'rxjs'; -import { DaffNewsletterSubmission } from '@daffodil/newsletter'; import { DaffNewsletterServiceInterface, DaffNewsletterDriver, @@ -17,8 +16,8 @@ import { import { DaffTestingNewsletterService } from '@daffodil/newsletter/driver/testing'; import { DaffNewsletterSubscribe, - DaffNewsletterSuccessSubscribe, - DaffNewsletterFailedSubscribe, + DaffNewsletterSubscribeSuccess, + DaffNewsletterSubscribeFailure, DaffNewsletterRetry, DaffNewsletterCancel, } from '@daffodil/newsletter/state'; @@ -28,12 +27,9 @@ import { DaffNewsletterEffects } from './newsletter.effects'; describe('NewsletterEffects', () => { let actions$: Observable; - let effects: DaffNewsletterEffects; - const mockNewsletter = { email: 'test@test.com' }; - let daffNewsletterDriver: DaffNewsletterServiceInterface< - DaffNewsletterSubmission, - any - >; + let effects: DaffNewsletterEffects; + const mockNewsletter = 'test@test.com'; + let daffNewsletterDriver: DaffNewsletterServiceInterface; beforeEach(() => { TestBed.configureTestingModule({ @@ -60,8 +56,8 @@ describe('NewsletterEffects', () => { describe('and the call to NewsletterService is successful', () => { it('it should dispatch a NewsletterSuccessSubscribe', () => { - const successAction = new DaffNewsletterSuccessSubscribe(); - spyOn(daffNewsletterDriver, 'send').and.returnValue(of('mystring')); + const successAction = new DaffNewsletterSubscribeSuccess(); + spyOn(daffNewsletterDriver, 'send').and.returnValue(of({ message: 'mystring' })); actions$ = hot('--a', { a: newsletterSubscribe }); expected = cold('--b', { b: successAction }); @@ -74,7 +70,7 @@ describe('NewsletterEffects', () => { const error = { code: 'code', recoverable: false, message: 'Failed to subscribe to newsletter' }; const response = cold('#', {}, error); spyOn(daffNewsletterDriver, 'send').and.returnValue(response); - const failedAction = new DaffNewsletterFailedSubscribe(error); + const failedAction = new DaffNewsletterSubscribeFailure([error]); actions$ = hot('--a', { a: newsletterSubscribe }); expected = cold('--b', { b: failedAction }); @@ -83,14 +79,14 @@ describe('NewsletterEffects', () => { }); }); - describe('when NewsletterRetry is triggered', () => { + describe('when Retry is triggered', () => { let expected; const newsletterRetry = new DaffNewsletterRetry(mockNewsletter); describe('and the call to NewsletterService is successful', () => { it('it should dispatch a NewsletterSuccessSubscribe', () => { - const successAction = new DaffNewsletterSuccessSubscribe(); - spyOn(daffNewsletterDriver, 'send').and.returnValue(of('mystring')); + const successAction = new DaffNewsletterSubscribeSuccess(); + spyOn(daffNewsletterDriver, 'send').and.returnValue(of({ message: 'mystring' })); actions$ = hot('--a', { a: newsletterRetry }); expected = cold('--b', { b: successAction }); @@ -103,7 +99,7 @@ describe('NewsletterEffects', () => { const error = { code: 'code', recoverable: false, message: 'Failed to subscribe to newsletter' }; const response = cold('#', {}, error); spyOn(daffNewsletterDriver, 'send').and.returnValue(response); - const failedAction = new DaffNewsletterFailedSubscribe(error); + const failedAction = new DaffNewsletterSubscribeFailure([error]); actions$ = hot('--a', { a: newsletterRetry }); expected = cold('--b', { b: failedAction }); diff --git a/libs/newsletter/state/src/effects/newsletter.effects.ts b/libs/newsletter/state/src/effects/newsletter.effects.ts index 42bbff4e8d..c1b30493b8 100644 --- a/libs/newsletter/state/src/effects/newsletter.effects.ts +++ b/libs/newsletter/state/src/effects/newsletter.effects.ts @@ -8,17 +8,19 @@ import { createEffect, } from '@ngrx/effects'; import { Action } from '@ngrx/store'; -import { of } from 'rxjs'; +import { + EMPTY, + of, +} from 'rxjs'; import { Observable } from 'rxjs'; import { switchMap, map, - catchError, } from 'rxjs/operators'; -import { DaffError } from '@daffodil/core'; +import { catchAndArrayifyErrors } from '@daffodil/core'; import { ErrorTransformer } from '@daffodil/core/state'; -import { DaffNewsletterSubmission } from '@daffodil/newsletter'; +import { DaffNewsletterResponse } from '@daffodil/newsletter'; import { DaffNewsletterDriver, DaffNewsletterServiceInterface, @@ -27,36 +29,37 @@ import { import { DaffNewsletterActionTypes, DaffNewsletterSubscribe, - DaffNewsletterSuccessSubscribe, - DaffNewsletterFailedSubscribe, + DaffNewsletterSubscribeSuccess, + DaffNewsletterSubscribeFailure, DaffNewsletterRetry, DaffNewsletterCancel, } from '../actions/newsletter.actions'; import { DAFF_NEWSLETTER_ERROR_MATCHER } from '../injection-tokens/public_api'; @Injectable() -export class DaffNewsletterEffects{ +export class DaffNewsletterEffects { constructor( private actions$: Actions, - @Inject(DaffNewsletterDriver) private driver: DaffNewsletterServiceInterface, + @Inject(DaffNewsletterDriver) private driver: DaffNewsletterServiceInterface, @Inject(DAFF_NEWSLETTER_ERROR_MATCHER) private errorMatcher: ErrorTransformer, ) { } trySubmission$: Observable = createEffect(() => this.actions$.pipe( - ofType(DaffNewsletterActionTypes.NewsletterSubscribeAction, - DaffNewsletterActionTypes.NewsletterRetry, - DaffNewsletterActionTypes.NewsletterCancelAction), - switchMap((action: DaffNewsletterSubscribe | DaffNewsletterRetry | DaffNewsletterCancel) => { - if ((action.type === DaffNewsletterActionTypes.NewsletterCancelAction)) { - return of(action); + ofType( + DaffNewsletterActionTypes.Subscribe, + DaffNewsletterActionTypes.Retry, + DaffNewsletterActionTypes.Cancel, + ), + switchMap((action: DaffNewsletterSubscribe | DaffNewsletterRetry | DaffNewsletterCancel) => { + if ((action.type === DaffNewsletterActionTypes.Cancel)) { + return EMPTY; } else if (action instanceof DaffNewsletterSubscribe || action instanceof DaffNewsletterRetry){ return this.driver.send(action.payload).pipe( - map((resp: V) => new DaffNewsletterSuccessSubscribe()), - catchError((error: DaffError) => of(new DaffNewsletterFailedSubscribe(this.errorMatcher(error)))), + map((resp: DaffNewsletterResponse) => new DaffNewsletterSubscribeSuccess()), + catchAndArrayifyErrors((errors) => of(new DaffNewsletterSubscribeFailure(errors.map(this.errorMatcher)))), ); } }), - ofType(DaffNewsletterActionTypes.NewsletterFailedSubscribeAction, DaffNewsletterActionTypes.NewsletterSuccessSubscribeAction), )); } diff --git a/libs/newsletter/state/src/facades/newsletter-facade.interface.ts b/libs/newsletter/state/src/facades/newsletter-facade.interface.ts index d38b10a7e0..ae767174f0 100644 --- a/libs/newsletter/state/src/facades/newsletter-facade.interface.ts +++ b/libs/newsletter/state/src/facades/newsletter-facade.interface.ts @@ -8,6 +8,6 @@ import { export interface DaffNewsletterFacadeInterface extends DaffStoreFacade { success$: Observable; - error$: Observable; + error$: Observable>; loading$: Observable; } diff --git a/libs/newsletter/state/src/facades/newsletter.facade.spec.ts b/libs/newsletter/state/src/facades/newsletter.facade.spec.ts index 7c57501a7b..710a0855d6 100644 --- a/libs/newsletter/state/src/facades/newsletter.facade.spec.ts +++ b/libs/newsletter/state/src/facades/newsletter.facade.spec.ts @@ -9,11 +9,11 @@ import { cold } from 'jasmine-marbles'; import { DaffNewsletterSubmission } from '@daffodil/newsletter'; import { DaffNewsletterSubscribe, - DaffNewsletterFailedSubscribe, - DaffNewsletterSuccessSubscribe, + DaffNewsletterSubscribeFailure, + DaffNewsletterSubscribeSuccess, DaffNewsletterStateRootSlice, DAFF_NEWSLETTER_STORE_FEATURE_KEY, - reducer, + daffNewsletterStateReducer, } from '@daffodil/newsletter/state'; import { DaffNewsletterFacade } from './newsletter.facade'; @@ -27,7 +27,7 @@ describe('DaffNewsletterFacade', () => { TestBed.configureTestingModule({ imports:[ StoreModule.forRoot({ - [DAFF_NEWSLETTER_STORE_FEATURE_KEY]: reducer, + [DAFF_NEWSLETTER_STORE_FEATURE_KEY]: daffNewsletterStateReducer, }), ], providers: [ @@ -59,21 +59,21 @@ describe('DaffNewsletterFacade', () => { it('should return true after a successful subscription', () => { const expected = cold('a', { a: true }); - store.dispatch(new DaffNewsletterSuccessSubscribe()); + store.dispatch(new DaffNewsletterSubscribeSuccess()); expect(facade.success$).toBeObservable(expected); }); }); describe('error$', () => { - it('should intially be null', () => { - const expected = cold('a', { a: null }); + it('should intially be an empty array', () => { + const expected = cold('a', { a: []}); expect(facade.error$).toBeObservable(expected); }); it('should return an error message when it fails to subscribe', () => { const error = { code: 'code', message: 'Failed to subscribe to newsletter' }; - const expected = cold('a', { a: error }); - store.dispatch(new DaffNewsletterFailedSubscribe(error)); + const expected = cold('a', { a: [error]}); + store.dispatch(new DaffNewsletterSubscribeFailure([error])); expect(facade.error$).toBeObservable(expected); }); }); @@ -86,7 +86,7 @@ describe('DaffNewsletterFacade', () => { it('it should be true if the newsletter is loading', () => { const expected = cold('a', { a: true }); - const payload: DaffNewsletterSubmission = { email: 'yes@gmail.com' }; + const payload: DaffNewsletterSubmission = 'yes@gmail.com'; store.dispatch(new DaffNewsletterSubscribe(payload)); expect(facade.loading$).toBeObservable(expected); }); diff --git a/libs/newsletter/state/src/facades/newsletter.facade.ts b/libs/newsletter/state/src/facades/newsletter.facade.ts index 9ecc8f3b0e..cc778871df 100644 --- a/libs/newsletter/state/src/facades/newsletter.facade.ts +++ b/libs/newsletter/state/src/facades/newsletter.facade.ts @@ -18,7 +18,7 @@ import { @Injectable({ providedIn: 'root' }) export class DaffNewsletterFacade implements DaffNewsletterFacadeInterface { success$: Observable = this.store.select(selectDaffNewsletterSuccess); - error$: Observable = this.store.select(selectDaffNewsletterError); + error$: Observable> = this.store.select(selectDaffNewsletterError); loading$: Observable = this.store.select(selectDaffNewsletterLoading); constructor(private store: Store){ diff --git a/libs/newsletter/state/src/newsletter-state.module.ts b/libs/newsletter/state/src/newsletter-state.module.ts index 6052e68e1a..7c4bce4c67 100644 --- a/libs/newsletter/state/src/newsletter-state.module.ts +++ b/libs/newsletter/state/src/newsletter-state.module.ts @@ -5,12 +5,12 @@ import { StoreModule } from '@ngrx/store'; import { DaffNewsletterEffects } from './effects/newsletter.effects'; import { DAFF_NEWSLETTER_STORE_FEATURE_KEY } from './reducers/newsletter-store-feature-key'; -import { reducer } from './reducers/newsletter.reducer'; +import { daffNewsletterStateReducer } from './reducers/newsletter.reducer'; @NgModule({ imports: [ CommonModule, - StoreModule.forFeature(DAFF_NEWSLETTER_STORE_FEATURE_KEY, reducer), + StoreModule.forFeature(DAFF_NEWSLETTER_STORE_FEATURE_KEY, daffNewsletterStateReducer), EffectsModule.forFeature([ DaffNewsletterEffects, ]), diff --git a/libs/newsletter/state/src/public_api.ts b/libs/newsletter/state/src/public_api.ts index 663318fe63..e407ef947f 100644 --- a/libs/newsletter/state/src/public_api.ts +++ b/libs/newsletter/state/src/public_api.ts @@ -2,8 +2,9 @@ export * from './actions/newsletter.actions'; export * from './injection-tokens/public_api'; export { DaffNewsletterState, - reducer, + daffNewsletterStateReducer, DaffNewsletterStateRootSlice, + daffNewsletterReducerInitialState, } from './reducers/newsletter.reducer'; export { DAFF_NEWSLETTER_STORE_FEATURE_KEY } from './reducers/newsletter-store-feature-key'; export { diff --git a/libs/newsletter/state/src/reducers/newsletter.reducer.spec.ts b/libs/newsletter/state/src/reducers/newsletter.reducer.spec.ts index 098be1c26f..5b39e60fb0 100644 --- a/libs/newsletter/state/src/reducers/newsletter.reducer.spec.ts +++ b/libs/newsletter/state/src/reducers/newsletter.reducer.spec.ts @@ -1,22 +1,28 @@ +import { + daffOperationInitialState, + DaffState, +} from '@daffodil/core/state'; import { DaffNewsletterSubmission } from '@daffodil/newsletter'; import { DaffNewsletterSubscribe, DaffNewsletterRetry, DaffNewsletterCancel, - DaffNewsletterFailedSubscribe, - DaffNewsletterSuccessSubscribe, + DaffNewsletterSubscribeFailure, + DaffNewsletterSubscribeSuccess, DaffNewsletterReset, DaffNewsletterState, } from '@daffodil/newsletter/state'; -import { reducer } from './newsletter.reducer'; +import { + daffNewsletterStateReducer as reducer, + daffNewsletterReducerInitialState as initialState, +} from './newsletter.reducer'; -describe('the newsletter reducer', () => { +describe('@daffodil/newsletter/state | reducer', () => { it('should create an initial state', () => { const expectedInitialState: DaffNewsletterState = { - error: null, - loading: false, + ...daffOperationInitialState, success: false, }; const action = {}; @@ -24,80 +30,56 @@ describe('the newsletter reducer', () => { }); it('should start loading when a subscription attempt occurs', () =>{ - const payload: DaffNewsletterSubmission = { email: 'yes@gmail.com' }; + const payload: DaffNewsletterSubmission = 'yes@gmail.com'; const action = new DaffNewsletterSubscribe(payload); - const expectedState = { - error: null, - loading: true, - success: false, - }; - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action).daffState).toEqual(DaffState.Updating); }); it('should start loading when a retry occurs', () =>{ - const payload: DaffNewsletterSubmission = { email: 'yes@gmail.com' }; + const payload: DaffNewsletterSubmission = 'yes@gmail.com'; const action = new DaffNewsletterRetry(payload); - const expectedState = { - error: null, - loading: true, - success: false, - }; - expect(reducer(undefined, action)).toEqual(expectedState); + expect(reducer(undefined, action).daffState).toEqual(DaffState.Updating); }); it('should cancel loading the newsletter', () => { const action = new DaffNewsletterCancel(); - const loadingState = { - error: null, - loading: true, - success: false, + const loadingState: DaffNewsletterState = { + ...initialState, + daffState: DaffState.Updating, }; - const expectedState = { - error: null, - loading: false, - success: false, - }; - expect(reducer(loadingState, action)).toEqual(expectedState); + expect(reducer(loadingState, action).daffState).toEqual(DaffState.Stable); }); it('should cancel loading and have an error message if the subscribe fails', () =>{ const error = { code: 'code', message: 'Failed to subscribe to newsletter' }; - const action = new DaffNewsletterFailedSubscribe(error); - const failedState = { - error: null, - loading: true, - success: false, - }; - const expectedState = { - error, - loading: false, + const action = new DaffNewsletterSubscribeFailure([error]); + const state: DaffNewsletterState = { + ...initialState, success: false, }; - expect(reducer(failedState, action)).toEqual(expectedState); + const result = reducer(state, action); + expect(result.success).toBeFalse(); + expect(result.daffState).toEqual(DaffState.Error); + expect(result.daffErrors).toContain(error); }); it('should set success to true after a successful subscription', () =>{ - const action = new DaffNewsletterSuccessSubscribe(); - const preSuccessState = { - error: null, - loading: true, + const action = new DaffNewsletterSubscribeSuccess(); + const preSuccessState: DaffNewsletterState = { + ...initialState, success: false, }; - const expectedState = { - error: null, - loading: false, - success: true, - }; - expect(reducer(preSuccessState, action)).toEqual(expectedState); + const result = reducer(preSuccessState, action); + expect(result.success).toBeTrue(); + expect(result.daffState).toEqual(DaffState.Stable); }); it('should return to the intialState when reset', () => { const action = new DaffNewsletterReset(); const successState = { - error: null, - loading: false, + ...initialState, success: true, }; - expect(reducer(successState, action)).toEqual(reducer(undefined,{})); + expect(reducer(successState, action)).toEqual(initialState); }); }); diff --git a/libs/newsletter/state/src/reducers/newsletter.reducer.ts b/libs/newsletter/state/src/reducers/newsletter.reducer.ts index f147e29787..b30f2cf552 100644 --- a/libs/newsletter/state/src/reducers/newsletter.reducer.ts +++ b/libs/newsletter/state/src/reducers/newsletter.reducer.ts @@ -1,5 +1,12 @@ -import { DaffStateError } from '@daffodil/core/state'; -import { DaffNewsletterSubmission } from '@daffodil/newsletter'; +import { ActionReducer } from '@ngrx/store'; + +import { + daffCompleteOperation, + daffOperationFailed, + daffOperationInitialState, + DaffOperationState, + daffStartMutation, +} from '@daffodil/core/state'; import { DaffNewsletterActions, @@ -7,36 +14,38 @@ import { } from './../actions/newsletter.actions'; import { DAFF_NEWSLETTER_STORE_FEATURE_KEY } from './newsletter-store-feature-key'; -export interface DaffNewsletterState { +export interface DaffNewsletterState extends DaffOperationState { success: boolean; - loading: boolean; - error: DaffStateError; } export interface DaffNewsletterStateRootSlice { [DAFF_NEWSLETTER_STORE_FEATURE_KEY]: DaffNewsletterState; } -const initialState: DaffNewsletterState = { +export const daffNewsletterReducerInitialState: DaffNewsletterState = { + ...daffOperationInitialState, success: false, - loading: false, - error: null, }; -export function reducer(state: DaffNewsletterState = initialState, action: DaffNewsletterActions) { +export const daffNewsletterStateReducer: ActionReducer = (state: DaffNewsletterState = daffNewsletterReducerInitialState, action: DaffNewsletterActions): DaffNewsletterState => { switch (action.type) { - case DaffNewsletterActionTypes.NewsletterRetry: - case DaffNewsletterActionTypes.NewsletterSubscribeAction: - return { ...state, loading: true }; - case DaffNewsletterActionTypes.NewsletterFailedSubscribeAction: - return { ...state, loading: false, error: action.payload }; - case DaffNewsletterActionTypes.NewsletterCancelAction: - return { ...state, loading: false }; - case DaffNewsletterActionTypes.NewsletterSuccessSubscribeAction: - return { ...state, success: true, loading: false }; - case DaffNewsletterActionTypes.NewsletterReset: - return { ...state, ...initialState }; + case DaffNewsletterActionTypes.Retry: + case DaffNewsletterActionTypes.Subscribe: + return daffStartMutation(state); + + case DaffNewsletterActionTypes.SubscribeFailure: + return daffOperationFailed(action.payload, state); + + case DaffNewsletterActionTypes.Cancel: + return daffCompleteOperation(state); + + case DaffNewsletterActionTypes.SubscribeSuccess: + return { ...daffCompleteOperation(state), success: true }; + + case DaffNewsletterActionTypes.Reset: + return daffNewsletterReducerInitialState; + default: return state; } -} +}; diff --git a/libs/newsletter/state/src/selectors/newsletter.selector.spec.ts b/libs/newsletter/state/src/selectors/newsletter.selector.spec.ts index 3f78108cbe..743f85c5c6 100644 --- a/libs/newsletter/state/src/selectors/newsletter.selector.spec.ts +++ b/libs/newsletter/state/src/selectors/newsletter.selector.spec.ts @@ -8,16 +8,13 @@ import { cold } from 'jasmine-marbles'; import { DaffNewsletterState, - reducer, + daffNewsletterStateReducer, DAFF_NEWSLETTER_STORE_FEATURE_KEY, DaffNewsletterStateRootSlice, + daffNewsletterReducerInitialState, } from '@daffodil/newsletter/state'; -import { - selectDaffNewsletterLoading, - selectDaffNewsletterSuccess, - selectDaffNewsletterError, -} from './newsletter.selector'; +import { selectDaffNewsletterSuccess } from './newsletter.selector'; describe('DaffNewsletterSelectors', () => { @@ -28,21 +25,13 @@ describe('DaffNewsletterSelectors', () => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({ - [DAFF_NEWSLETTER_STORE_FEATURE_KEY]: reducer, + [DAFF_NEWSLETTER_STORE_FEATURE_KEY]: daffNewsletterStateReducer, }), ], }); - mockNewsletter = { loading: false, success: false, error: null }; - store = TestBed.inject(Store); - - }); - describe('selectDaffNewsletterLoading', () =>{ - it('selects the loading property of newsletter state', () => { - const selector = store.pipe(select(selectDaffNewsletterLoading)); - const expected = cold('a', { a: mockNewsletter.loading }); - expect(selector).toBeObservable(expected); - }); + mockNewsletter = daffNewsletterReducerInitialState; + store = TestBed.inject(Store); }); describe('selectDaffNewsletterSuccess', () =>{ @@ -52,12 +41,4 @@ describe('DaffNewsletterSelectors', () => { expect(selector).toBeObservable(expected); }); }); - - describe('selectDaffNewsletterError', () =>{ - it('selects the error property of newsletter state', () => { - const selector = store.pipe(select(selectDaffNewsletterError)); - const expected = cold('a', { a: mockNewsletter.error }); - expect(selector).toBeObservable(expected); - }); - }); }); diff --git a/libs/newsletter/state/src/selectors/newsletter.selector.ts b/libs/newsletter/state/src/selectors/newsletter.selector.ts index 4c0b03acfd..6fc88b9b03 100644 --- a/libs/newsletter/state/src/selectors/newsletter.selector.ts +++ b/libs/newsletter/state/src/selectors/newsletter.selector.ts @@ -2,10 +2,9 @@ import { createSelector, MemoizedSelector, createFeatureSelector, - DefaultProjectorFn, } from '@ngrx/store'; -import { DaffStateError } from '@daffodil/core/state'; +import { daffOperationStateSelectorFactory } from '@daffodil/core/state'; import { DAFF_NEWSLETTER_STORE_FEATURE_KEY } from '../reducers/newsletter-store-feature-key'; import { @@ -19,19 +18,10 @@ import { const selectNewsletterFeatureState: MemoizedSelector = createFeatureSelector(DAFF_NEWSLETTER_STORE_FEATURE_KEY); - -/** - * Child key of feature state - */ -export const selectDaffNewsletterLoading = createSelector( - selectNewsletterFeatureState, - (state: DaffNewsletterState) => state.loading, -); - -export const selectDaffNewsletterError: MemoizedSelector> = createSelector( - selectNewsletterFeatureState, - (state: DaffNewsletterState) => state.error, -); +export const { + selectErrors: selectDaffNewsletterError, + selectLoading: selectDaffNewsletterLoading, +} = daffOperationStateSelectorFactory(selectNewsletterFeatureState); export const selectDaffNewsletterSuccess = createSelector( selectNewsletterFeatureState, diff --git a/libs/newsletter/state/testing/src/mock-newsletter-facade.ts b/libs/newsletter/state/testing/src/mock-newsletter-facade.ts index d26003e2fe..d8175486aa 100644 --- a/libs/newsletter/state/testing/src/mock-newsletter-facade.ts +++ b/libs/newsletter/state/testing/src/mock-newsletter-facade.ts @@ -8,7 +8,7 @@ import { DaffNewsletterFacadeInterface } from '@daffodil/newsletter/state'; @Injectable({ providedIn: 'root' }) export class MockDaffNewsletterFacade implements DaffNewsletterFacadeInterface { success$: BehaviorSubject = new BehaviorSubject(false); - error$: BehaviorSubject = new BehaviorSubject(null); + error$: BehaviorSubject> = new BehaviorSubject([]); loading$: BehaviorSubject = new BehaviorSubject(false); dispatch(action: Action) {}