From 136b00559bf6288e9120f27959b5b3f3a9df0ccb Mon Sep 17 00:00:00 2001 From: Clark Malmgren Date: Mon, 14 Aug 2017 17:25:49 -0500 Subject: [PATCH] Add more testing! (#13 still matters) --- src/app/components/organisms/index.ts | 4 +- src/app/components/organisms/ustream.html | 5 - src/app/components/organisms/ustream.scss | 6 - src/app/components/organisms/ustream.ts | 9 -- src/app/components/pages/giving.scss | 4 +- src/app/components/pages/sermons/sermon.html | 4 +- .../components/pages/sermons/sermon.spec.ts | 131 ++++++++++++++++++ src/app/components/pages/sermons/sermon.ts | 26 +--- .../components/templates/autoclean.spec.ts | 22 +++ src/app/components/templates/autoclean.ts | 2 +- .../templates/service-group.spec.ts | 71 ++++++++++ src/app/components/templates/service-group.ts | 3 +- src/testing.ts | 16 ++- 13 files changed, 248 insertions(+), 55 deletions(-) delete mode 100644 src/app/components/organisms/ustream.html delete mode 100644 src/app/components/organisms/ustream.scss delete mode 100644 src/app/components/organisms/ustream.ts create mode 100644 src/app/components/pages/sermons/sermon.spec.ts create mode 100644 src/app/components/templates/autoclean.spec.ts create mode 100644 src/app/components/templates/service-group.spec.ts diff --git a/src/app/components/organisms/index.ts b/src/app/components/organisms/index.ts index baea0d5..b2919a6 100644 --- a/src/app/components/organisms/index.ts +++ b/src/app/components/organisms/index.ts @@ -1,11 +1,9 @@ import { FooterComponent } from './footer'; import { HeaderComponent } from './header'; import { SermonListComponent } from './sermon-list'; -import { UStreamComponent } from './ustream'; export const ORGANISMS = [ FooterComponent, HeaderComponent, - SermonListComponent, - UStreamComponent + SermonListComponent ]; diff --git a/src/app/components/organisms/ustream.html b/src/app/components/organisms/ustream.html deleted file mode 100644 index d1a706c..0000000 --- a/src/app/components/organisms/ustream.html +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/src/app/components/organisms/ustream.scss b/src/app/components/organisms/ustream.scss deleted file mode 100644 index 8aad268..0000000 --- a/src/app/components/organisms/ustream.scss +++ /dev/null @@ -1,6 +0,0 @@ -iframe { - border: 0 none transparent; - width: 640px; - height: 480px; - max-width: 95% -} \ No newline at end of file diff --git a/src/app/components/organisms/ustream.ts b/src/app/components/organisms/ustream.ts deleted file mode 100644 index 27aa03f..0000000 --- a/src/app/components/organisms/ustream.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'bc-ustream', - templateUrl: './ustream.html', - styleUrls: ['./ustream.scss'] -}) -export class UStreamComponent { -} diff --git a/src/app/components/pages/giving.scss b/src/app/components/pages/giving.scss index 9b8c005..1d4a861 100644 --- a/src/app/components/pages/giving.scss +++ b/src/app/components/pages/giving.scss @@ -19,8 +19,8 @@ div.giving-list { text-decoration: none; margin: 25px; - min-width: 130px; - max-width: 130px; + min-width: 150px; + max-width: 150px; button { background-color: white; diff --git a/src/app/components/pages/sermons/sermon.html b/src/app/components/pages/sermons/sermon.html index 055f130..4c87d76 100644 --- a/src/app/components/pages/sermons/sermon.html +++ b/src/app/components/pages/sermons/sermon.html @@ -1,7 +1,7 @@
- - -
diff --git a/src/app/components/pages/sermons/sermon.spec.ts b/src/app/components/pages/sermons/sermon.spec.ts new file mode 100644 index 0000000..76eb633 --- /dev/null +++ b/src/app/components/pages/sermons/sermon.spec.ts @@ -0,0 +1,131 @@ +import { expect, sinon, async, MockBuilder } from 'testing'; +import { ActivatedRoute } from '@angular/router'; +import { SermonComponent } from './sermon'; +import { + Analytics, + FeatureToggles, + Observable, + SeriesImageService, + Sermon, + SermonService, + VideoState, + YoutubeService +} from 'app/services'; + +describe('SermonComponent', () => { + + describe('ngOnInit', () => { + + it('should subscribe to video state changes', async(() => { + const youtubeService = MockBuilder.of(YoutubeService) + .withStub('videoState', Observable.of(VideoState.BUFFERING)) + .build(); + + const activatedRoute = MockBuilder.of(ActivatedRoute) + .with('params', Observable.empty()) + .build(); + + const sermon = new SermonComponent(activatedRoute, null, null, null, null, null, youtubeService); + + sermon.ngOnInit(); + + expect(youtubeService.videoState).to.have.been.calledOnce.and.calledWith('sermonVideo'); + expect(sermon.videoState).to.equal(VideoState.BUFFERING); + + sermon.ngOnDestroy(); + })); + + it('should register analytics events on an appropriate cadence', async(() => { + const youtubeService = MockBuilder.of(YoutubeService) + .withStub('videoState', Observable.empty()) + .build(); + + const activatedRoute = MockBuilder.of(ActivatedRoute) + .with('params', Observable.empty()) + .build(); + + const analytics = MockBuilder.of(Analytics) + .withSpy('event') + .build(); + + const sermon = new SermonComponent(activatedRoute, analytics, null, null, null, null, youtubeService); + sermon.analyticsInterval = 10; + sermon.videoState = VideoState.PLAYING; + sermon.sermon = { youtube: 'Jesus4Life' } as Sermon; + + sermon.ngOnInit(); + + window.setTimeout(() => { + expect(analytics.event).to.have.been.calledTwice + .and.to.have.been.calledWith('Sermon', 'Playing', 'Jesus4Life'); + sermon.ngOnDestroy(); + }, 25); + })); + + it('should register live analytics events as appropriate', async(() => { + const youtubeService = MockBuilder.of(YoutubeService) + .withStub('videoState', Observable.empty()) + .build(); + + const activatedRoute = MockBuilder.of(ActivatedRoute) + .with('params', Observable.empty()) + .build(); + + const analytics = MockBuilder.of(Analytics) + .withSpy('event') + .build(); + + const sermon = new SermonComponent(activatedRoute, analytics, null, null, null, null, youtubeService); + sermon.analyticsInterval = 10; + sermon.videoState = VideoState.PLAYING; + sermon.live = true; + sermon.sermon = { youtube: 'Jesus4Life' } as Sermon; + + sermon.ngOnInit(); + + window.setTimeout(() => { + expect(analytics.event).to.have.been.calledWith('Live Sermon', 'Playing', 'Jesus4Life'); + sermon.ngOnDestroy(); + }, 25); + })); + + [ + VideoState.BUFFERING, + VideoState.CUED, + VideoState.ENDED, + VideoState.PAUSED, + VideoState.UNSTARTED + ].forEach(state => { + it(`should not call analytics when video state is "${state}"`, async(() => { + const youtubeService = MockBuilder.of(YoutubeService) + .withStub('videoState', Observable.empty()) + .build(); + + const activatedRoute = MockBuilder.of(ActivatedRoute) + .with('params', Observable.empty()) + .build(); + + const analytics = MockBuilder.of(Analytics) + .withSpy('event') + .build(); + + const sermon = new SermonComponent(activatedRoute, analytics, null, null, null, null, youtubeService); + sermon.analyticsInterval = 10; + sermon.videoState = state; + sermon.sermon = { youtube: 'Jesus4Life' } as Sermon; + + sermon.ngOnInit(); + + window.setTimeout(() => { + expect(analytics.event).to.not.have.been.called; + sermon.ngOnDestroy(); + }, 25); + })); + }); + + + + + }); + +}); diff --git a/src/app/components/pages/sermons/sermon.ts b/src/app/components/pages/sermons/sermon.ts index a0104e0..1a9bbbf 100644 --- a/src/app/components/pages/sermons/sermon.ts +++ b/src/app/components/pages/sermons/sermon.ts @@ -10,7 +10,6 @@ import { SeriesImageService, Sermon, SermonService, - TogglesService, VideoState, YoutubeService } from '../../../services'; @@ -27,13 +26,12 @@ export class SermonComponent extends Autoclean implements OnInit { youtube_url: SafeResourceUrl; icon: SafeStyle; - youtube_live: boolean = false; videoState: VideoState = VideoState.UNSTARTED; + analyticsInterval: number = 60000; constructor( private activatedRoute: ActivatedRoute, private analytics: Analytics, - private featureToggles: TogglesService, private imageService: SeriesImageService, private meta: Meta, private sanitizer: DomSanitizer, @@ -43,27 +41,7 @@ export class SermonComponent extends Autoclean implements OnInit { super(); } - get showYoutube() { - if (this.live) { - return this.youtube_live; - } else { - return this.sermon && this.youtube_url; - } - } - - get showUstream(): boolean { - return this.live && !this.youtube_live; - } - ngOnInit() { - /* Subscribe to Toggles */ - this.autoclean( - this.featureToggles - .getToggles() - .subscribe((toggles) => { - this.youtube_live = toggles.youtube_live; - })); - /* Subscribe to Video State Change Events */ this.autoclean( this.youtubeService @@ -74,7 +52,7 @@ export class SermonComponent extends Autoclean implements OnInit { /* Record Analytics when Playing */ this.autoclean( - Observable.interval(60000) + Observable.interval(this.analyticsInterval) .subscribe(() => { if (this.videoState === VideoState.PLAYING) { this.analytics.event(this.live ? 'Live Sermon' : 'Sermon', 'Playing', this.sermon.youtube); diff --git a/src/app/components/templates/autoclean.spec.ts b/src/app/components/templates/autoclean.spec.ts new file mode 100644 index 0000000..f320efe --- /dev/null +++ b/src/app/components/templates/autoclean.spec.ts @@ -0,0 +1,22 @@ +import { expect, sinon, async, MockBuilder, Loop } from 'testing'; +import { Autoclean } from './autoclean'; +import { Subscription } from 'app/services'; + +class TestableAutoclean extends Autoclean {} + +describe('Autoclean', () => { + + it('should clean up registered subscriptions', () => { + const ac = new TestableAutoclean(); + const subscription = MockBuilder.of(Subscription) + .withSpy('unsubscribe') + .build(); + + Loop.times(10).do(() => ac.autoclean(subscription)); + + ac.ngOnDestroy(); + + expect(subscription.unsubscribe).to.have.callCount(10); + }); + +}); diff --git a/src/app/components/templates/autoclean.ts b/src/app/components/templates/autoclean.ts index 1a55ee4..fa26546 100644 --- a/src/app/components/templates/autoclean.ts +++ b/src/app/components/templates/autoclean.ts @@ -1,5 +1,5 @@ import { OnDestroy } from '@angular/core'; -import { Subscription } from '../../services'; +import { Subscription } from 'app/services'; export abstract class Autoclean implements OnDestroy { diff --git a/src/app/components/templates/service-group.spec.ts b/src/app/components/templates/service-group.spec.ts new file mode 100644 index 0000000..9b65dca --- /dev/null +++ b/src/app/components/templates/service-group.spec.ts @@ -0,0 +1,71 @@ +import { expect, sinon, async, MockBuilder } from 'testing'; +import { Router } from '@angular/router'; +import { ServiceGroup } from './service-group'; +import { + Analytics, + ConnectionRequest, + ConnectionService, + Observable +} from '../../services'; + +class TestableServiceGroup extends ServiceGroup {} + +describe('ServiceGroup', () => { + + describe('constructor', () => { + it('should work', () => { + const sg = new TestableServiceGroup( + undefined, + undefined, + undefined, + 'hero', + 'title', + 'sub', + [ 'it does this', 'and this' ], + { + a: { name : 'Eh', description: 'Aye' }, + b: { name: 'Bee', description: 'Honey' } + } + ); + + expect(sg).to.exist; + expect(sg.typeKeys).to.have.lengthOf(2); + expect(sg.typesArray).to.have.lengthOf(2); + }); + }); + + describe('submit', () => { + it('should submit exactly once', async(() => { + const service = MockBuilder.of(ConnectionService) + .withStub('submit', Observable.of('')) + .build(); + + const analytics = MockBuilder.of(Analytics) + .withStub('event', Observable.of('')) + .build(); + + const router = MockBuilder.of(Router) + .withSpy('navigate') + .build(); + + const sg = new TestableServiceGroup( + service, + router, + analytics, + undefined, + undefined, + undefined, + undefined, + {} + ); + + sg.submit() + .subscribe(() => { + expect(service.submit).to.have.been.calledOnce; + expect(analytics.event).to.have.been.calledOnce; + expect(router.navigate).to.have.been.calledOnce.and.be.calledWith([ '/thank-you' ]); + }); + })); + }); + +}); diff --git a/src/app/components/templates/service-group.ts b/src/app/components/templates/service-group.ts index 7370dbc..c88c4e0 100644 --- a/src/app/components/templates/service-group.ts +++ b/src/app/components/templates/service-group.ts @@ -41,7 +41,8 @@ export abstract class ServiceGroup { this.request.interests = Object.keys(this.interests).map(i => this.types[i].name); const o = this.service.submit(this.request) .flatMap(() => { return this.analytics.event('form', 'submit', this.title); }) - .catch((err) => { return this.analytics.exception(err); }); + .catch((err) => { return this.analytics.exception(err); }) + .shareReplay(); o.subscribe(() => { this.router.navigate([ '/thank-you' ]); diff --git a/src/testing.ts b/src/testing.ts index dda8b46..6ea590b 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -15,7 +15,7 @@ export class MockBuilder { return new MockBuilder(); } - with(key: string, fn: Function): MockBuilder { + with(key: string, fn: any): MockBuilder { const keys = key.split('.'); let o = this.object; @@ -42,6 +42,20 @@ export class MockBuilder { } } +export class Loop { + static times(n: number) { + return new Loop(n); + } + + constructor(private times: number) {} + + do(fn: Function): void { + for (let i = 0; i < this.times; i++) { + fn(); + } + } +} + export { async, expect,