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..1a775e7
--- /dev/null
+++ b/src/app/components/pages/sermons/sermon.spec.ts
@@ -0,0 +1,132 @@
+import { expect, sinon, async, MockBuilder, callCount } 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 = 5;
+ sermon.videoState = VideoState.PLAYING;
+ sermon.sermon = { youtube: 'Jesus4Life' } as Sermon;
+
+ sermon.ngOnInit();
+
+ window.setTimeout(() => {
+ /* Giving a reasonable range to account for annoying time based testing */
+ expect(callCount(analytics.event)).to.be.at.least(2).and.at.most(6);
+ expect(analytics.event).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 = 5;
+ 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 = 5;
+ 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..4434f6d 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,24 @@ export class MockBuilder {
}
}
+export function callCount(spy: any) {
+ return spy.callCount;
+}
+
+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,