diff --git a/CHANGELOG.md b/CHANGELOG.md index 8819729..dfcba15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ + +# 1.9.0 (2016-07-10) + +### Features + +* **Unit Test**: Break out storage and use DI to inject it, attempting to address [#86](https://github.com/lathonez/clicker/issues/86) ([](https://github.com/lathonez/clicker/commit/)) + # 1.8.1 (2016-07-04) diff --git a/app/app.ts b/app/app.ts index e738a90..1066a24 100644 --- a/app/app.ts +++ b/app/app.ts @@ -1,9 +1,10 @@ 'use strict'; -import { Component, Type, ViewChild } from '@angular/core'; -import { ionicBootstrap, MenuController, Nav, Platform } from 'ionic-angular'; -import { StatusBar } from 'ionic-native'; -import { ClickerList, Page2 } from './pages'; +import { Component, provide, Type, ViewChild } from '@angular/core'; +import { ionicBootstrap, MenuController, Nav, Platform } from 'ionic-angular'; +import { StatusBar } from 'ionic-native'; +import { Clickers, Storage } from './services'; +import { ClickerList, Page2 } from './pages'; @Component({ templateUrl: 'build/app.html', @@ -48,4 +49,4 @@ export class ClickerApp { }; } -ionicBootstrap(ClickerApp); +ionicBootstrap(ClickerApp, [Clickers, provide('Storage', {useClass: Storage})]); diff --git a/app/pages/clickerList/clickerList.spec.ts b/app/pages/clickerList/clickerList.spec.ts index 80b7d69..3e1be1a 100644 --- a/app/pages/clickerList/clickerList.spec.ts +++ b/app/pages/clickerList/clickerList.spec.ts @@ -9,7 +9,7 @@ this.fixture = null; this.instance = null; let clickerListProviders: Array = [ - provide(Clickers, {useClass: ClickersMock}), + provide(Clickers, {useClass: ClickersMock}), ]; describe('ClickerList', () => { diff --git a/app/pages/clickerList/clickerList.ts b/app/pages/clickerList/clickerList.ts index e3b7559..6dcaeec 100644 --- a/app/pages/clickerList/clickerList.ts +++ b/app/pages/clickerList/clickerList.ts @@ -7,7 +7,6 @@ import { ClickerButton, ClickerForm } from '../../components'; @Component({ templateUrl: 'build/pages/clickerList/clickerList.html', - providers: [Clickers], directives: [ClickerButton, ClickerForm], }) diff --git a/app/services/clickers.mock.ts b/app/services/clickers.mock.ts index 6d6e304..7ca1665 100644 --- a/app/services/clickers.mock.ts +++ b/app/services/clickers.mock.ts @@ -8,4 +8,8 @@ export class ClickersMock { public newClicker(): boolean { return true; } + + public getClickers(): Array { + return []; + } } diff --git a/app/services/clickers.spec.ts b/app/services/clickers.spec.ts index 08168c6..9b8f87d 100644 --- a/app/services/clickers.spec.ts +++ b/app/services/clickers.spec.ts @@ -1,138 +1,95 @@ -import { Clickers } from './clickers'; -import { Clicker } from '../models/clicker'; - -const CLICKER_IDS: Array = ['yy5d8klsj0', 'q20iexxg4a', 'wao2xajl8a']; -let clickers: Clickers = null; - -function storageGetStub(key: string): Promise<{}> { - 'use strict'; - - let rtn: string = null; - - switch (key) { - case 'ids': - rtn = JSON.stringify(CLICKER_IDS); - break; - case CLICKER_IDS[0]: - rtn = '{"id":"' + CLICKER_IDS[0] + '","name":"test1","clicks":[{"time":1450410168819,"location":"TODO"}]}'; - break; - case CLICKER_IDS[1]: - rtn = '{"id":"' + CLICKER_IDS[1] + '","name":"test2","clicks":[{"time":1450410168819,"location":"TODO"},{"time":1450410168945,"location":"TODO"}]}'; - break; - case CLICKER_IDS[2]: - rtn = '{"id":"' + CLICKER_IDS[2] + '", "name":"test3", "clicks":[{ "time": 1450410168819, "location": "TODO" }, { "time": 1450410168945, "location": "TODO" }] } '; - break; - default: - rtn = 'SHOULD NOT BE HERE!'; - } - - return new Promise((resolve: Function) => { - resolve(rtn); - }); -} - -function storageSetStub(): Promise<{}> { - 'use strict'; - - return new Promise((resolve: Function) => { - resolve(true); - }); -} - -function storageRemoveStub(): Promise<{}> { - 'use strict'; - - return new Promise((resolve: Function) => { - resolve(true); - }); -} - -let mockSqlStorage: Object = { - get: storageGetStub, - set: storageSetStub, - remove: storageRemoveStub, -}; +import { beforeEach, beforeEachProviders, describe, expect, it } from '@angular/core/testing'; +import { provide } from '@angular/core'; +import { asyncCallbackFactory, injectAsyncWrapper, providers } from '../../test/diExports'; +import { Clickers } from './clickers'; +import { Clicker } from '../models/clicker'; +import { ClickerList } from '../pages/clickerList/clickerList'; +import { StorageMock } from './mocks'; + +this.fixture = null; +this.instance = null; +this.clickers = null; + +let clickerListProviders: Array = [ + Clickers, + provide('Storage', { useClass: StorageMock }), +]; + +let beforeEachFn: Function = ((testSpec) => { + testSpec.clickers = testSpec.instance.clickerService; + spyOn(testSpec.clickers.storage, 'set').and.callThrough(); +}); describe('Clickers', () => { - beforeEach(() => { - spyOn(Clickers, 'initStorage').and.returnValue(mockSqlStorage); - clickers = new Clickers(); - spyOn(clickers['storage'], 'set'); - }); + beforeEachProviders(() => providers.concat(clickerListProviders)); + beforeEach(injectAsyncWrapper(asyncCallbackFactory(ClickerList, this, false, beforeEachFn))); - it('initialises with empty clickers', () => { - expect(clickers.getClickers()).toEqual([]); + it('initialises', () => { + expect(this.clickers).not.toBeNull(); + expect(this.instance).not.toBeNull(); + expect(this.fixture).not.toBeNull(); }); - it('creates an instance of SqlStorage', () => { - expect((Clickers).initStorage()).toEqual(mockSqlStorage); - }); - - it('has empty ids with no storage', (done: Function) => { - (clickers).initIds() - .then(() => { - expect(clickers.getClickers()).toEqual([]); - done(); - }); + it('initialises with empty clickers', () => { + expect(new Clickers(null).getClickers()).toEqual([]); }); - it('has empty clickers with no storage', (done: Function) => { - (clickers).initClickers([]) + it('initialises with clickers from mock storage', (done: Function) => { + this.clickers['initClickers']([]) .then(() => { - expect(clickers.getClickers()).toEqual([]); + expect(this.clickers.getClickers().length).toEqual(StorageMock.CLICKER_IDS.length); done(); }); }); it('can initialise a clicker from string', () => { let clickerString: string = '{"id":"0g2vt8qtlm","name":"harold","clicks":[{"time":1450410168819,"location":"TODO"},{"time":1450410168945,"location":"TODO"}]}'; - let clicker: Clicker = (clickers).initClicker(clickerString); + let clicker: Clicker = this.clickers.initClicker(clickerString); expect(clicker.getName()).toEqual('harold'); expect(clicker.getCount()).toEqual(2); }); it('returns undefined for a bad id', () => { - expect(clickers.getClicker('dave')).not.toBeDefined(); + expect(this.clickers.getClicker('dave')).not.toBeDefined(); }); it('adds a new clicker with the correct name', () => { - let idAdded: string = clickers.newClicker('dave'); - expect(clickers['storage'].set).toHaveBeenCalledWith(idAdded, jasmine.any(String)); - expect(clickers.getClickers()[0].getName()).toEqual('dave'); + let idAdded: string = this.clickers.newClicker('dave'); + expect(this.clickers['storage'].set).toHaveBeenCalledWith(idAdded, jasmine.any(String)); + expect(this.clickers.getClickers()[3].getName()).toEqual('dave'); }); it('removes a clicker by id', () => { - let idToRemove: string = clickers.newClicker('dave'); - clickers.removeClicker(idToRemove); - expect(clickers['storage'].set).toHaveBeenCalledWith(idToRemove, jasmine.any(String)); - expect(clickers.getClickers()).toEqual([]); + let idToRemove: string = this.clickers.newClicker('dave'); + this.clickers.removeClicker(idToRemove); + expect(this.clickers['storage'].set).toHaveBeenCalledWith(idToRemove, jasmine.any(String)); }); it('does a click', () => { - let idToClick: string = clickers.newClicker('dave'); + let idToClick: string = this.clickers.newClicker('dave'); let clickedClicker: Clicker = null; - clickers.doClick(idToClick); - expect(clickers['storage'].set).toHaveBeenCalledWith(idToClick, jasmine.any(String)); - clickedClicker = clickers.getClicker(idToClick); + this.clickers.doClick(idToClick); + expect(this.clickers['storage'].set).toHaveBeenCalledWith(idToClick, jasmine.any(String)); + clickedClicker = this.clickers.getClicker(idToClick); expect(clickedClicker.getCount()).toEqual(1); }); it('loads IDs from storage', (done: Function) => { - (clickers).initIds() + this.clickers.initIds() .then((ids: Array) => { - expect(ids).toEqual(CLICKER_IDS); + expect(ids).toEqual(StorageMock.CLICKER_IDS); done(); }); }); it('loads clickers from storage', (done: Function) => { - (clickers).initClickers(CLICKER_IDS) + this.clickers.initClickers(StorageMock.CLICKER_IDS) .then((resolvedClickers: Array) => { expect(resolvedClickers.length).toEqual(3); - expect(resolvedClickers[0].getId()).toEqual(CLICKER_IDS[0]); - expect(resolvedClickers[1].getId()).toEqual(CLICKER_IDS[1]); - expect(resolvedClickers[2].getId()).toEqual(CLICKER_IDS[2]); + expect(resolvedClickers[0].getId()).toEqual(StorageMock.CLICKER_IDS[0]); + expect(resolvedClickers[1].getId()).toEqual(StorageMock.CLICKER_IDS[1]); + expect(resolvedClickers[2].getId()).toEqual(StorageMock.CLICKER_IDS[2]); done(); }); }); diff --git a/app/services/clickers.ts b/app/services/clickers.ts index 8308676..310cb79 100644 --- a/app/services/clickers.ts +++ b/app/services/clickers.ts @@ -1,18 +1,20 @@ 'use strict'; -import { Injectable } from '@angular/core'; -import { SqlStorage } from 'ionic-angular'; -import { Click, Clicker } from '../models'; +import { Inject, Injectable } from '@angular/core'; +import { Storage } from './'; +import { Click, Clicker } from '../models'; @Injectable() export class Clickers { private clickers: Array; private ids: Array; // we need to keep a separate reference to ids so we can lookup when the app loads from scratch - private storage: SqlStorage; + private storage: Storage; - constructor() { - this.storage = Clickers.initStorage(); // typeof SqlStorage is not assignable to type StorageEngine seems to be an ionic issue + // don't know why Injection isn't working without @Inject: + // http://stackoverflow.com/questions/34449486/angular-2-0-injected-http-service-is-undefined + constructor(@Inject('Storage') storage: Storage) { + this.storage = storage; this.ids = []; this.clickers = []; this.initIds() @@ -49,6 +51,7 @@ export class Clickers { clickers.push(this.initClicker(clicker)); }); } + // TODO - this is a bug it will resolve before the loop has completed resolve(clickers); }); } @@ -66,10 +69,6 @@ export class Clickers { return newClicker; } - private static initStorage(): SqlStorage { - return new SqlStorage(); - } - public getClicker(id: string): Clicker { return this.clickers['find']((clicker: Clicker) => { return clicker.getId() === id; } ); } diff --git a/app/services/index.ts b/app/services/index.ts index 38105e8..5c8b138 100644 --- a/app/services/index.ts +++ b/app/services/index.ts @@ -1,2 +1,3 @@ export * from './clickers'; +export * from './storage'; export * from './utils'; diff --git a/app/services/mocks.ts b/app/services/mocks.ts index 04e7683..851dcb2 100644 --- a/app/services/mocks.ts +++ b/app/services/mocks.ts @@ -1 +1,2 @@ export * from './clickers.mock'; +export * from './storage.mock'; diff --git a/app/services/storage.mock.ts b/app/services/storage.mock.ts new file mode 100644 index 0000000..6c34b76 --- /dev/null +++ b/app/services/storage.mock.ts @@ -0,0 +1,43 @@ +'use strict'; + +export class StorageMock { + + public static CLICKER_IDS: Array = ['yy5d8klsj0', 'q20iexxg4a', 'wao2xajl8a']; + + public get(key: string): Promise<{}> { + let rtn: string = null; + + switch (key) { + case 'ids': + rtn = JSON.stringify(StorageMock.CLICKER_IDS); + break; + case StorageMock.CLICKER_IDS[0]: + rtn = `{"id":"${StorageMock.CLICKER_IDS[0]}","name":"test1","clicks":[{"time":1450410168819,"location":"TODO"}]}`; + break; + case StorageMock.CLICKER_IDS[1]: + rtn = `{"id":"${StorageMock.CLICKER_IDS[1]}","name":"test2","clicks":[{"time":1450410168819,"location":"TODO"},{"time":1450410168945,"location":"TODO"}]}`; + break; + case StorageMock.CLICKER_IDS[2]: + rtn = `{"id":"${StorageMock.CLICKER_IDS[2]}","name":"test3", "clicks":[{ "time": 1450410168819, "location": "TODO" }, { "time": 1450410168945, "location": "TODO" }] }`; + break; + default: + rtn = 'SHOULD NOT BE HERE!'; + } + + return new Promise((resolve: Function) => { + resolve(rtn); + }); + } + + public set(key: string, value: string): Promise<{}> { + return new Promise((resolve: Function) => { + resolve({key: key, value: value}); + }); + } + + public remove(key: string): Promise<{}> { + return new Promise((resolve: Function) => { + resolve({key: key}); + }); + } +} diff --git a/app/services/storage.spec.ts b/app/services/storage.spec.ts new file mode 100644 index 0000000..9e1c80f --- /dev/null +++ b/app/services/storage.spec.ts @@ -0,0 +1,34 @@ +import { Storage } from './'; +import { StorageMock } from './mocks'; + +let storage: Storage = null; + +describe('Storage', () => { + + beforeEach(() => { + spyOn(Storage, 'initStorage').and.returnValue(new StorageMock()); + storage = new Storage(); + spyOn(storage['storage'], 'get').and.callThrough(); + spyOn(storage['storage'], 'set').and.callThrough(); + spyOn(storage['storage'], 'remove').and.callThrough(); + }); + + it('initialises', () => { + expect(storage).not.toBeNull(); + }); + + it('gets', () => { + storage.get('dave'); + expect(storage['storage'].get).toHaveBeenCalledWith('dave'); + }); + + it('sets', () => { + storage.set('dave', 'test'); + expect(storage['storage'].set).toHaveBeenCalledWith('dave', 'test'); + }); + + it('removes', () => { + storage.remove('dave'); + expect(storage['storage'].remove).toHaveBeenCalledWith('dave'); + }); +}); diff --git a/app/services/storage.ts b/app/services/storage.ts new file mode 100644 index 0000000..104173b --- /dev/null +++ b/app/services/storage.ts @@ -0,0 +1,28 @@ +'use strict'; + +import { SqlStorage } from 'ionic-angular'; + +export class Storage { + + private storage: SqlStorage; + + constructor() { + this.storage = Storage.initStorage(); + } + + private static initStorage(): SqlStorage { + return new SqlStorage(); + } + + public get(key: string): Promise<{}> { + return this.storage.get(key); + } + + public set(key: string, value: string): Promise<{}> { + return this.storage.set(key, value); + } + + public remove(key: string): Promise<{}> { + return this.storage.remove(key); + } +} diff --git a/app/services/utils.spec.ts b/app/services/utils.spec.ts index 7667a7b..796bdae 100644 --- a/app/services/utils.spec.ts +++ b/app/services/utils.spec.ts @@ -1,10 +1,6 @@ -import { Utils } from './utils'; +import { Utils } from './'; import { AbstractControl, Control } from '@angular/common'; -import { - describe, - expect, - it, -} from '@angular/core/testing'; + describe('Utils', () => { it('resets a control', () => {