Skip to content

Commit

Permalink
Breakout storage and use DI to inject it properly, attempting to solve
Browse files Browse the repository at this point in the history
  • Loading branch information
lathonez committed Jul 10, 2016
1 parent a7ce1e7 commit 297dd00
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 116 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<a name="1.9.0"></a>
# 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/))

<a name="1.8.1"></a>
# 1.8.1 (2016-07-04)

Expand Down
11 changes: 6 additions & 5 deletions app/app.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -48,4 +49,4 @@ export class ClickerApp {
};
}

ionicBootstrap(ClickerApp);
ionicBootstrap(ClickerApp, [Clickers, provide('Storage', {useClass: Storage})]);
2 changes: 1 addition & 1 deletion app/pages/clickerList/clickerList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ this.fixture = null;
this.instance = null;

let clickerListProviders: Array<any> = [
provide(Clickers, {useClass: ClickersMock}),
provide(Clickers, {useClass: ClickersMock}),
];

describe('ClickerList', () => {
Expand Down
1 change: 0 additions & 1 deletion app/pages/clickerList/clickerList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ClickerButton, ClickerForm } from '../../components';

@Component({
templateUrl: 'build/pages/clickerList/clickerList.html',
providers: [Clickers],
directives: [ClickerButton, ClickerForm],
})

Expand Down
4 changes: 4 additions & 0 deletions app/services/clickers.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export class ClickersMock {
public newClicker(): boolean {
return true;
}

public getClickers(): Array<string> {
return [];
}
}
143 changes: 50 additions & 93 deletions app/services/clickers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,138 +1,95 @@
import { Clickers } from './clickers';
import { Clicker } from '../models/clicker';

const CLICKER_IDS: Array<string> = ['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<any> = [
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((<any>Clickers).initStorage()).toEqual(mockSqlStorage);
});

it('has empty ids with no storage', (done: Function) => {
(<any>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) => {
(<any>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 = (<any>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) => {
(<any>clickers).initIds()
this.clickers.initIds()
.then((ids: Array<string>) => {
expect(ids).toEqual(CLICKER_IDS);
expect(ids).toEqual(StorageMock.CLICKER_IDS);
done();
});
});

it('loads clickers from storage', (done: Function) => {
(<any>clickers).initClickers(CLICKER_IDS)
this.clickers.initClickers(StorageMock.CLICKER_IDS)
.then((resolvedClickers: Array<Clicker>) => {
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();
});
});
Expand Down
19 changes: 9 additions & 10 deletions app/services/clickers.ts
Original file line number Diff line number Diff line change
@@ -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<Clicker>;
private ids: Array<string>; // 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()
Expand Down Expand Up @@ -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);
});
}
Expand All @@ -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; } );
}
Expand Down
1 change: 1 addition & 0 deletions app/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './clickers';
export * from './storage';
export * from './utils';
1 change: 1 addition & 0 deletions app/services/mocks.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './clickers.mock';
export * from './storage.mock';
43 changes: 43 additions & 0 deletions app/services/storage.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

export class StorageMock {

public static CLICKER_IDS: Array<string> = ['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});
});
}
}
34 changes: 34 additions & 0 deletions app/services/storage.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit 297dd00

Please sign in to comment.