diff --git a/src/index.test.js b/src/index.test.js new file mode 100644 index 0000000..c7fde20 --- /dev/null +++ b/src/index.test.js @@ -0,0 +1,12 @@ +/* global jest, test, expect */ +import bluetooth from '.'; + +jest.mock('./webapp', () => null); +jest.mock('./peripheral', () => null); + +test('actions', () => { + expect(Object.keys(bluetooth)).toEqual([ + 'peripheral', + 'webapp', + ]); +}); diff --git a/src/peripheral/index.test.js b/src/peripheral/index.test.js new file mode 100644 index 0000000..21f5a54 --- /dev/null +++ b/src/peripheral/index.test.js @@ -0,0 +1,11 @@ +/* global jest, test, expect */ +import peripheral from '.'; + +jest.mock('./bleno', () => ({ + start: jest.fn().mockReturnValue('mockStart'), +})); + +test('peripheral', () => { + const result = peripheral(null, null); + expect(result).toEqual('mockStart'); +}); diff --git a/src/webapp/central/central.js b/src/webapp/central/central.js new file mode 100644 index 0000000..ccac90c --- /dev/null +++ b/src/webapp/central/central.js @@ -0,0 +1,52 @@ +export default function Central( + bluetooth, + { encode, decode }, + { SERVICE_UUID, CHARACTERISTIC_UUID }) { + const state = { + server: null, + characteristic: null, + }; + + const connect = name => bluetooth + .requestDevice({ + filters: [{ services: [SERVICE_UUID], name }], + }) + .then(device => device.gatt.connect()) + .then((server) => { + state.server = server; + return server.getPrimaryService(SERVICE_UUID); + }) + .then(service => service.getCharacteristic(CHARACTERISTIC_UUID)) + .then((characteristic) => { + state.characteristic = characteristic; + }); + + const handler = callback => state.characteristic.startNotifications().then(() => { + const listerner = event => callback(decode(event.target.value)); + state.characteristic.addEventListener('characteristicvaluechanged', listerner); + return listerner; + }); + + const read = () => { + if (state.server && state.server.connected && state.characteristic) { + return state.characteristic.readValue().then(data => decode(data)); + } + return Promise.reject(new Error('Bluetooth: Not Connected')); + }; + + const write = (action) => { + if (!state.server || !state.server.connected || !state.characteristic) return null; + const stringify = JSON.stringify(action); + const serialized = encode(stringify); + + return state.characteristic.writeValue(serialized); + }; + + return { + connected: state.server && state.server.connected, + connect, + handler, + read, + write, + }; +} diff --git a/src/webapp/central/central.test.js b/src/webapp/central/central.test.js new file mode 100644 index 0000000..fdec0d8 --- /dev/null +++ b/src/webapp/central/central.test.js @@ -0,0 +1,100 @@ +/* global jest, beforeEach, afterEach, test, expect */ +import { CENTRAL_CONFIG } from '../../common/config'; +import Central from './central'; + +const encoder = { + encode: jest.fn().mockReturnValue('mockEncode'), + decode: jest.fn().mockReturnValue('mockDecode'), +}; + +const characteristic = { + startNotifications: jest.fn().mockReturnValue(Promise.resolve()), + addEventListener: jest.fn(), + writeValue: jest.fn().mockReturnValue(Promise.resolve()), + readValue: jest.fn().mockReturnValue(Promise.resolve('mockData')), +}; + +const service = { + getCharacteristic: jest.fn().mockReturnValue(Promise.resolve(characteristic)), +}; + +const server = { + connected: true, + getPrimaryService: jest.fn().mockReturnValue(Promise.resolve(service)), +}; + +const device = { + gatt: { + connect: jest.fn().mockReturnValue(Promise.resolve(server)), + }, +}; + +const bluetooth = { + requestDevice: jest.fn().mockReturnValue(Promise.resolve(device)), +}; + +let central = null; + +beforeEach(() => { + central = new Central(bluetooth, encoder, CENTRAL_CONFIG); +}); + +afterEach(() => { + central = null; +}); + + +test('Central: connect', () => { + expect.assertions(5); + + const promise = central.connect('mockName').then(() => { + expect(bluetooth.requestDevice).toBeCalled(); + expect(device.gatt.connect).toBeCalled(); + expect(server.getPrimaryService).toBeCalled(); + expect(service.getCharacteristic).toBeCalled(); + return true; + }); + + return expect(promise).resolves.toBe(true); +}); + +test('Central: handler', () => { + const callback = jest.fn(); + expect.assertions(3); + + const promise = central.connect('mockName').then(() => central.handler(callback)) + .then((listerner) => { + expect(characteristic.startNotifications).toBeCalled(); + listerner({ target: { value: 'mockEvent' } }); + expect(callback).toBeCalledWith('mockDecode'); + return true; + }); + + return expect(promise).resolves.toBe(true); +}); + +test('Central: read', () => { + expect.assertions(2); + + const promise = central.connect('mockName').then(() => central.read()) + .then((data) => { + expect(characteristic.readValue).toBeCalled(); + return data; + }); + + return expect(promise).resolves.toBe('mockDecode'); +}); + +test('Central: write', () => { + expect.assertions(2); + + const promise = central.connect('mockName') + .then(() => central.write({ type: 'ACTION' })) + .then(() => { + expect(characteristic.writeValue).toBeCalledWith('mockEncode'); + return true; + }); + + return expect(promise).resolves.toBe(true); +}); + diff --git a/src/webapp/central/index.js b/src/webapp/central/index.js index 401800a..8dcd158 100644 --- a/src/webapp/central/index.js +++ b/src/webapp/central/index.js @@ -1,57 +1,9 @@ +/* global navigator TextEncoder TextDecoder */ import { CENTRAL_CONFIG } from '../../common/config'; import Encoder from '../../common/encoder'; -export function Central(bluetooth, { encode, decode }, { SERVICE_UUID, CHARACTERISTIC_UUID }) { - const state = { - server: null, - characteristic: null, - }; +import Central from './central'; - const connect = name => bluetooth - .requestDevice({ - filters: [{ services: [SERVICE_UUID], name }], - }) - .then(device => device.gatt.connect()) - .then((server) => { - state.server = server; - return server.getPrimaryService(SERVICE_UUID); - }) - .then(service => service.getCharacteristic(CHARACTERISTIC_UUID)) - .then((characteristic) => { - state.characteristic = characteristic; - }); - - const handler = callback => state.characteristic.startNotifications().then(() => { - state.characteristic.addEventListener('characteristicvaluechanged', (event) => { - callback(decode(event.target.value)); - }); - }); - - const read = () => { - if (state.server && state.server.connected && state.characteristic) { - return state.characteristic.readValue().then(data => decode(data)); - } - return Promise.reject(new Error('Bluetooth: Not Connected')); - }; - - const write = (action) => { - if (!state.server || !state.server.connected || !state.characteristic) return null; - const stringify = JSON.stringify(action); - const serialized = encode(stringify); - - return state.characteristic.writeValue(serialized); - }; - - return { - connected: state.server && state.server.connected, - connect, - handler, - read, - write, - }; -} - -/* global navigator TextEncoder TextDecoder */ export default new Central( navigator.bluetooth, Encoder({ TextEncoder, TextDecoder }), diff --git a/src/webapp/middleware/index.test.js b/src/webapp/middleware/index.test.js new file mode 100644 index 0000000..9293224 --- /dev/null +++ b/src/webapp/middleware/index.test.js @@ -0,0 +1,37 @@ +/* global jest, test, expect, beforeEach, afterEach */ +import middleware from '.'; + +jest.mock('../actions', () => ({ + sendAction: jest.fn().mockReturnValue('mockAction'), +})); + +let store = null; +let next = null; + +beforeEach(() => { + store = { + dispatch: jest.fn(), + }; + next = jest.fn(); +}); + +afterEach(() => { + store = null; + next = null; +}); + +test('actions empty', () => { + middleware()(store)(next)({ type: 'ACTION' }); + expect(next).toBeCalledWith({ type: 'ACTION' }); +}); + +test('actions not included', () => { + middleware(['ACTION'])(store)(next)({ type: 'NOTEXISTS' }); + expect(next).toBeCalledWith({ type: 'NOTEXISTS' }); +}); + +test('actions included', () => { + middleware(['ACTION'])(store)(next)({ type: 'ACTION' }); + expect(store.dispatch).toBeCalledWith('mockAction'); + expect(next).toBeCalledWith({ type: 'ACTION' }); +}); diff --git a/src/webapp/reducers/index.test.js b/src/webapp/reducers/index.test.js index 7ce81b5..18e72d4 100644 --- a/src/webapp/reducers/index.test.js +++ b/src/webapp/reducers/index.test.js @@ -1,6 +1,18 @@ /* global test, expect */ +import * as STATUS from '../central/status'; +import * as TYPES from '../actions/types'; + +import initial from './initial'; import Reducer from '.'; +test('Default State', () => { + const reducer = Reducer(); + + const nextState = reducer(undefined, { type: 'UNKNOWN' }); + + return expect(nextState).toBe(initial); +}); + test('type: UNKNOWN', () => { const reducer = Reducer(); @@ -9,3 +21,40 @@ test('type: UNKNOWN', () => { return expect(nextState).toBe(originalState); }); + +test('type: BLUETOOTH_CONNECTING', () => { + const reducer = Reducer(); + + const originalState = { }; + const nextState = reducer(originalState, { type: TYPES.BLUETOOTH_CONNECTING }); + + return expect(nextState).toEqual({ status: STATUS.CONNECTING }); +}); + +test('type: BLUETOOTH_CONNECTED', () => { + const reducer = Reducer(); + + const originalState = { }; + const nextState = reducer(originalState, { type: TYPES.BLUETOOTH_CONNECTED }); + + return expect(nextState).toEqual({ status: STATUS.CONNECTED }); +}); + +test('type: BLUETOOTH_SYNC', () => { + const reducer = Reducer(); + + const originalState = { }; + const nextState = reducer(originalState, { type: TYPES.BLUETOOTH_SYNC, payload: 'mockPayload' }); + + return expect(nextState).toEqual({ store: 'mockPayload' }); +}); + +test('type: BLUETOOTH_SYNC, autosync: false', () => { + const reducer = Reducer(false); + + const originalState = { }; + const nextState = reducer(originalState, { type: TYPES.BLUETOOTH_SYNC, payload: 'mockPayload' }); + + return expect(nextState).toEqual({ }); +}); + diff --git a/src/webapp/reducers/initial.js b/src/webapp/reducers/initial.js index e69de29..3868f3c 100644 --- a/src/webapp/reducers/initial.js +++ b/src/webapp/reducers/initial.js @@ -0,0 +1,3 @@ +import * as STATUS from '../central/status'; + +export default { status: STATUS.INIT }; diff --git a/src/webapp/store/index.test.js b/src/webapp/store/index.test.js new file mode 100644 index 0000000..ec8c866 --- /dev/null +++ b/src/webapp/store/index.test.js @@ -0,0 +1,16 @@ +/* global jest, test, expect */ +import createSyncStore from '.'; + +jest.mock('redux', () => ({ + createStore: jest.fn().mockReturnValue('mockStore'), + applyMiddleware: jest.fn(), + compose: jest.fn(), +})); + +jest.mock('../middleware', () => jest.fn()); +jest.mock('../reducers', () => jest.fn()); + +test('createSyncStore', () => { + const store = createSyncStore(['ACTION']); + return expect(store).toBe('mockStore'); +});