From aa03b16916df5b791fe63a2dd4209ddbbb53e893 Mon Sep 17 00:00:00 2001 From: jvallelunga Date: Thu, 26 Oct 2017 00:50:42 +0200 Subject: [PATCH] [ISSUE-11] Notify Message Limit (#12) * [ISSUE-11] Notify Message Limit * [ISSUE-11] Unit Testing --- example/peripheral/src/output.jsx | 9 ++++-- example/peripheral/src/reducer.js | 12 +++++-- example/webapp/package.json | 3 +- example/webapp/src/actions/index.js | 11 +++++-- example/webapp/src/app/component.jsx | 20 +++++++----- example/webapp/src/app/index.js | 5 ++- example/webapp/src/app/style.css | 2 +- src/common/config/index.js | 4 +++ src/common/encoder/index.js | 12 ++----- src/common/encoder/index.test.js | 12 ++----- src/peripheral/bleno/characteristic.js | 28 ++++++++--------- src/peripheral/bleno/characteristic.test.js | 35 +++++---------------- src/webapp/actions/actions.js | 19 ++++++----- src/webapp/actions/actions.test.js | 24 ++------------ src/webapp/actions/index.test.js | 1 - src/webapp/central/central.js | 14 ++++++++- src/webapp/central/central.test.js | 6 ++-- src/webapp/index.js | 4 +-- src/webapp/middleware/index.js | 2 -- 19 files changed, 103 insertions(+), 120 deletions(-) diff --git a/example/peripheral/src/output.jsx b/example/peripheral/src/output.jsx index 141069e..f4d0e14 100644 --- a/example/peripheral/src/output.jsx +++ b/example/peripheral/src/output.jsx @@ -4,7 +4,7 @@ const { Provider, connect } = require('ink-redux'); const PropTypes = require('prop-types'); export default (store) => { - function Counter({ counter }) { + function Counter({ counter, quote }) { const color = { blue: counter > 0, red: counter < 0, @@ -13,19 +13,24 @@ export default (store) => {
Counter: {counter} +
+ {quote}
); } Counter.propTypes = { counter: PropTypes.number, + quote: PropTypes.string, }; Counter.defaultProps = { counter: 0, + quote: '', }; const mapStateToProps = state => ({ - counter: state, + counter: state.counter, + quote: state.quote, }); const ConnectedCounter = connect(mapStateToProps)(Counter); diff --git a/example/peripheral/src/reducer.js b/example/peripheral/src/reducer.js index 4117bd0..fef1036 100644 --- a/example/peripheral/src/reducer.js +++ b/example/peripheral/src/reducer.js @@ -1,9 +1,15 @@ -export default function counter(state = 0, { type }) { +export default function counter(state = { counter: 0 }, { type, payload }) { switch (type) { case 'INCREMENT': - return state + 1; + return { + counter: state.counter + 1, + quote: payload, + }; case 'DECREMENT': - return state - 1; + return { + counter: state.counter - 1, + quote: payload, + }; default: return state; } diff --git a/example/webapp/package.json b/example/webapp/package.json index 4942ba9..5233138 100644 --- a/example/webapp/package.json +++ b/example/webapp/package.json @@ -9,7 +9,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "predeploy": "npm run build", - "deploy": "gh-pages -d build" + "deploy": "gh-pages -d build" }, "repository": { "type": "git", @@ -28,6 +28,7 @@ }, "homepage": "https://jvallelunga.github.io/redux-bluetooth", "dependencies": { + "predator-quotes": "^1.1.16", "prop-types": "^15.5.10", "react": "^15.6.1", "react-dom": "^15.6.1", diff --git a/example/webapp/src/actions/index.js b/example/webapp/src/actions/index.js index ccc657e..fdcd6ab 100644 --- a/example/webapp/src/actions/index.js +++ b/example/webapp/src/actions/index.js @@ -1,9 +1,16 @@ +import predatorQuotes from 'predator-quotes'; import * as TYPES from './types'; export function increment() { - return { type: TYPES.INCREMENT }; + return { + type: TYPES.INCREMENT, + payload: predatorQuotes.random(), + }; } export function decrement() { - return { type: TYPES.DECREMENT }; + return { + type: TYPES.DECREMENT, + payload: predatorQuotes.random(), + }; } diff --git a/example/webapp/src/app/component.jsx b/example/webapp/src/app/component.jsx index fcf67ab..509e98a 100644 --- a/example/webapp/src/app/component.jsx +++ b/example/webapp/src/app/component.jsx @@ -16,18 +16,20 @@ export default class App extends PureComponent { } render() { - const { store, status, onIncrement, onDecrement } = this.props; + const { counter, quote, status, onIncrement, onDecrement } = this.props; - const counter = Number(store); + const nCounter = Number(counter); let className = 'app-counter'; - if (counter > 0) className += ' app-counter--positive'; - if (counter < 0) className += ' app-counter--negative'; + if (nCounter > 0) className += ' app-counter--positive'; + if (nCounter < 0) className += ' app-counter--negative'; return (
{status === 'CONNECTED' && -
- {store} +
+ {counter} +
+ {quote}
}
{status !== 'CONNECTED' && @@ -49,7 +51,8 @@ export default class App extends PureComponent { } App.propTypes = { - store: PropTypes.number, + counter: PropTypes.number, + quote: PropTypes.string, status: PropTypes.string, onConnect: PropTypes.func, onIncrement: PropTypes.func, @@ -57,7 +60,8 @@ App.propTypes = { }; App.defaultProps = { - store: 0, + counter: 0, + quote: '', status: '', onConnect: () => true, onIncrement: () => true, diff --git a/example/webapp/src/app/index.js b/example/webapp/src/app/index.js index c93cc0b..6c66bcd 100644 --- a/example/webapp/src/app/index.js +++ b/example/webapp/src/app/index.js @@ -5,7 +5,10 @@ import { increment, decrement } from '../actions'; import Component from './component'; -const mapState = state => state; +const mapState = ({ status, store }) => ({ + status, + ...store, +}); const mapAction = { onConnect: actions.connectStore, diff --git a/example/webapp/src/app/style.css b/example/webapp/src/app/style.css index fbe8fa1..91e2642 100644 --- a/example/webapp/src/app/style.css +++ b/example/webapp/src/app/style.css @@ -10,7 +10,7 @@ .app-counter { margin-bottom: 40px; - line-height: calc(100vh - 200px); + line-height: calc(100vh - 300px); font-size: 80vh; color: black; text-align: center; diff --git a/src/common/config/index.js b/src/common/config/index.js index 28a8af2..163fe27 100644 --- a/src/common/config/index.js +++ b/src/common/config/index.js @@ -9,3 +9,7 @@ export const BLENO_CONFIG = { CHARACTERISTIC_UUID: CONFIG.CHARACTERISTIC_UUID.replace(/-/g, ''), DESCRIPTOR_UUID: '2901', }; + +export const COMMON_TYPES = { + BLUETOOTH_SYNC_REQUEST: '@@bluetooth/SYNC_REQUEST', +}; diff --git a/src/common/encoder/index.js b/src/common/encoder/index.js index 515d435..070fac5 100644 --- a/src/common/encoder/index.js +++ b/src/common/encoder/index.js @@ -2,16 +2,8 @@ export default function Encoder({ TextEncoder, TextDecoder }) { const encoder = new TextEncoder('utf-8'); const decoder = new TextDecoder('utf-8'); - const encode = (json) => { - const string = JSON.stringify(json); - return encoder.encode(string); - }; - - const decode = (data) => { - const string = decoder.decode(data); - const json = JSON.parse(string); - return typeof json === 'string' ? JSON.parse(json) : json; - }; + const encode = string => encoder.encode(string); + const decode = data => decoder.decode(data); return { encode, decode }; } diff --git a/src/common/encoder/index.test.js b/src/common/encoder/index.test.js index 18461a3..0042f65 100644 --- a/src/common/encoder/index.test.js +++ b/src/common/encoder/index.test.js @@ -6,16 +6,10 @@ import Encoder from '.'; const { encode, decode } = new Encoder(TextEncoding); test('encode / decode', () => { - const data = encode({ type: 'TEST', payload: 'PAYLOAD' }); + const message = JSON.stringify({ type: 'TEST', payload: 'PAYLOAD' }); + const data = encode(message); const result = decode(data); - expect(result).toEqual({ type: 'TEST', payload: 'PAYLOAD' }); + expect(result).toEqual(message); }); - -test('encode / decode: string', () => { - const data = encode('{ "type": "TEST", "payload": "PAYLOAD" }'); - const result = decode(data); - - expect(result).toEqual({ type: 'TEST', payload: 'PAYLOAD' }); -}); diff --git a/src/peripheral/bleno/characteristic.js b/src/peripheral/bleno/characteristic.js index 0900135..b395ac8 100644 --- a/src/peripheral/bleno/characteristic.js +++ b/src/peripheral/bleno/characteristic.js @@ -2,11 +2,9 @@ export default function Characteristic(uuid, Parent, util, descriptor, { encode, function ReduxCharacteristic() { Parent.call(this, { uuid, - properties: ['read', 'write', 'notify'], + properties: ['write', 'notify'], descriptors: [descriptor], }); - - this.state = null; } util.inherits(ReduxCharacteristic, Parent); @@ -18,21 +16,14 @@ export default function Characteristic(uuid, Parent, util, descriptor, { encode, return; } - this.onAction(decode(data)); + const action = decode(data); + this.onAction(JSON.parse(action)); callback(this.RESULT_SUCCESS); }; - ReduxCharacteristic.prototype.onReadRequest = function (offset, callback) { - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null); - return; - } - - callback(this.RESULT_SUCCESS, this.state); - }; - ReduxCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) { + this.maxValueSize = maxValueSize; this.updateValueCallback = updateValueCallback; }; @@ -45,9 +36,16 @@ export default function Characteristic(uuid, Parent, util, descriptor, { encode, }; ReduxCharacteristic.prototype.updateState = function (state) { - this.state = encode(state); if (this.updateValueCallback) { - this.updateValueCallback(this.state); + const message = encode(`[[[${JSON.stringify(state)}]]]`); + let i = 0; + do { + const next = i + this.maxValueSize; + const end = Math.min(next, message.length); + const data = message.slice(i, end); + this.updateValueCallback(data); + i = next; + } while (i < message.length); } }; diff --git a/src/peripheral/bleno/characteristic.test.js b/src/peripheral/bleno/characteristic.test.js index 69e823a..c3867c9 100644 --- a/src/peripheral/bleno/characteristic.test.js +++ b/src/peripheral/bleno/characteristic.test.js @@ -16,7 +16,7 @@ let decode = null; beforeEach(() => { encode = jest.fn().mockReturnValue('mockEncode'); - decode = jest.fn().mockReturnValue('mockDecode'); + decode = jest.fn().mockReturnValue('{"mockDecode":"mockDecode"}'); }); afterEach(() => { @@ -28,9 +28,8 @@ test('new Characteristic', () => { const characteristic = Characteristic('mockUUID', Parent, util, 'mockDescriptor', { encode, decode }); expect(characteristic.uuid).toBe('mockUUID'); - expect(characteristic.properties).toEqual(['read', 'write', 'notify']); + expect(characteristic.properties).toEqual(['write', 'notify']); expect(characteristic.descriptors).toEqual(['mockDescriptor']); - expect(characteristic.state).toBe(null); }); test('Characteristic.onWriteRequest: RESULT_ATTR_NOT_LONG', () => { @@ -51,35 +50,17 @@ test('Characteristic.onWriteRequest: RESULT_SUCCESS', () => { const spyOnAction = jest.spyOn(characteristic, 'onAction'); characteristic.onWriteRequest(null, false, false, callback); - expect(spyOnAction).toBeCalledWith('mockDecode'); + expect(spyOnAction).toBeCalledWith({ mockDecode: 'mockDecode' }); expect(callback).toBeCalledWith('RESULT_SUCCESS'); callback = jest.fn(); characteristic.onAction = jest.fn(); characteristic.onWriteRequest(null, false, false, callback); - expect(characteristic.onAction).toBeCalledWith('mockDecode'); + expect(characteristic.onAction).toBeCalledWith({ mockDecode: 'mockDecode' }); expect(callback).toBeCalledWith('RESULT_SUCCESS'); }); -test('Characteristic.onReadRequest: RESULT_ATTR_NOT_LONG', () => { - const characteristic = Characteristic('mockUUID', Parent, util, 'mockDescriptor', { encode, decode }); - - const callback = jest.fn(); - characteristic.onReadRequest(true, callback); - - expect(callback).toBeCalledWith('RESULT_ATTR_NOT_LONG', null); -}); - -test('Characteristic.onReadRequest: RESULT_SUCCESS', () => { - const characteristic = Characteristic('mockUUID', Parent, util, 'mockDescriptor', { encode, decode }); - - const callback = jest.fn(); - characteristic.onReadRequest(false, callback); - - expect(callback).toBeCalledWith('RESULT_SUCCESS', characteristic.state); -}); - test('Characteristic.onSubscribe', () => { const characteristic = Characteristic('mockUUID', Parent, util, 'mockDescriptor', { encode, decode }); @@ -100,12 +81,10 @@ test('Characteristic.onUnsubscribe', () => { test('Characteristic.updateState', () => { const characteristic = Characteristic('mockUUID', Parent, util, 'mockDescriptor', { encode, decode }); - characteristic.updateState('mockState'); - expect(characteristic.state).toBe('mockEncode'); - + const maxValueSize = 10; const updateValueCallback = jest.fn(); - characteristic.onSubscribe(null, updateValueCallback); - characteristic.updateState('mockState'); + characteristic.onSubscribe(maxValueSize, updateValueCallback); + characteristic.updateState({ mockState: 'mockState' }); expect(updateValueCallback).toBeCalledWith('mockEncode'); }); diff --git a/src/webapp/actions/actions.js b/src/webapp/actions/actions.js index 665fe83..0101e80 100644 --- a/src/webapp/actions/actions.js +++ b/src/webapp/actions/actions.js @@ -4,9 +4,9 @@ export default function Actions(central, TYPES) { payload: state, }); - const syncStore = () => ({ - type: TYPES.BLUETOOTH_SYNC_REQUEST, - request: dispatch => central.read().then(state => dispatch(syncState(state))), + const sendAction = action => ({ + type: TYPES.BLUETOOTH_SEND_REQUEST, + request: () => central.write(action), }); const connectStore = name => ({ @@ -17,18 +17,17 @@ export default function Actions(central, TYPES) { .connect(name) .then(() => central.handler(state => dispatch(syncState(state)))) .then(() => dispatch({ type: TYPES.BLUETOOTH_CONNECTED })) - .then(() => dispatch(syncStore())); + .then(() => dispatch(sendAction({ + type: TYPES.BLUETOOTH_SEND_REQUEST, + request: () => central.write({ + type: TYPES.BLUETOOTH_SYNC_REQUEST, + }), + }))); }, }); - const sendAction = action => ({ - type: TYPES.BLUETOOTH_SEND_REQUEST, - request: () => central.write(action), - }); - return { connectStore, - syncStore, syncState, sendAction, }; diff --git a/src/webapp/actions/actions.test.js b/src/webapp/actions/actions.test.js index b59af06..fae57de 100644 --- a/src/webapp/actions/actions.test.js +++ b/src/webapp/actions/actions.test.js @@ -33,7 +33,7 @@ test('syncState', () => { test('connectStore', () => { const { connectStore } = Actions(central, TYPES); const action = connectStore('mockName'); - expect.assertions(10); + expect.assertions(9); expect(action.type).toEqual(TYPES.BLUETOOTH_CONNECT_REQUEST); @@ -46,28 +46,10 @@ test('connectStore', () => { expect(dispatch.mock.calls[1][0]).toEqual({ type: TYPES.BLUETOOTH_CONNECTED }); const syncStore = dispatch.mock.calls[2][0]; - expect(syncStore.type).toEqual(TYPES.BLUETOOTH_SYNC_REQUEST); + expect(syncStore.type).toEqual(TYPES.BLUETOOTH_SEND_REQUEST); return syncStore.request(dispatch); }).then(() => { - expect(central.read).toBeCalled(); - expect(dispatch.mock.calls[3][0]).toEqual({ type: TYPES.BLUETOOTH_SYNC, payload: 'mockState' }); - - return true; - }); - - return expect(promise).resolves.toBe(true); -}); - -test('syncStore', () => { - const { syncStore } = Actions(central, TYPES); - const action = syncStore(); - expect.assertions(4); - - expect(action.type).toEqual(TYPES.BLUETOOTH_SYNC_REQUEST); - - const promise = action.request(dispatch).then(() => { - expect(central.read).toBeCalled(); - expect(dispatch).toBeCalledWith({ type: TYPES.BLUETOOTH_SYNC, payload: 'mockState' }); + expect(central.write).toBeCalled(); return true; }); diff --git a/src/webapp/actions/index.test.js b/src/webapp/actions/index.test.js index 41b02f4..fc019c5 100644 --- a/src/webapp/actions/index.test.js +++ b/src/webapp/actions/index.test.js @@ -6,7 +6,6 @@ jest.mock('../central', () => null); test('actions', () => { expect(Object.keys(actions)).toEqual([ 'connectStore', - 'syncStore', 'syncState', 'sendAction', ]); diff --git a/src/webapp/central/central.js b/src/webapp/central/central.js index ccac90c..bad0ab7 100644 --- a/src/webapp/central/central.js +++ b/src/webapp/central/central.js @@ -5,6 +5,7 @@ export default function Central( const state = { server: null, characteristic: null, + message: '', }; const connect = name => bluetooth @@ -22,7 +23,18 @@ export default function Central( }); const handler = callback => state.characteristic.startNotifications().then(() => { - const listerner = event => callback(decode(event.target.value)); + const listerner = (event) => { + const chunk = decode(event.target.value); + const message = `${state.message}${chunk}`; + + if (message.startsWith('[[[') && message.endsWith(']]]')) { + const json = JSON.parse(message.slice(3, message.length - 3)); + callback(json); + state.message = ''; + } else { + state.message = message; + } + }; state.characteristic.addEventListener('characteristicvaluechanged', listerner); return listerner; }); diff --git a/src/webapp/central/central.test.js b/src/webapp/central/central.test.js index fdec0d8..48d974c 100644 --- a/src/webapp/central/central.test.js +++ b/src/webapp/central/central.test.js @@ -4,7 +4,7 @@ import Central from './central'; const encoder = { encode: jest.fn().mockReturnValue('mockEncode'), - decode: jest.fn().mockReturnValue('mockDecode'), + decode: jest.fn().mockReturnValue('[[[{"mockDecode":"mockDecode"}]]]'), }; const characteristic = { @@ -66,7 +66,7 @@ test('Central: handler', () => { .then((listerner) => { expect(characteristic.startNotifications).toBeCalled(); listerner({ target: { value: 'mockEvent' } }); - expect(callback).toBeCalledWith('mockDecode'); + expect(callback).toBeCalledWith({ mockDecode: 'mockDecode' }); return true; }); @@ -82,7 +82,7 @@ test('Central: read', () => { return data; }); - return expect(promise).resolves.toBe('mockDecode'); + return expect(promise).resolves.toBe('[[[{"mockDecode":"mockDecode"}]]]'); }); test('Central: write', () => { diff --git a/src/webapp/index.js b/src/webapp/index.js index 2f39dea..ca0098b 100644 --- a/src/webapp/index.js +++ b/src/webapp/index.js @@ -5,11 +5,11 @@ import MIDDLEWARE from './middleware'; import REDUCERS from './reducers'; import STORE from './store'; -const { connectStore, syncStore } = ACTIONS; +const { connectStore } = ACTIONS; export const types = TYPES; export const status = STATUS; -export const actions = { connectStore, syncStore }; +export const actions = { connectStore }; export const reducers = REDUCERS; export const middleware = MIDDLEWARE; export const createSyncStore = STORE; diff --git a/src/webapp/middleware/index.js b/src/webapp/middleware/index.js index 30beb87..1021295 100644 --- a/src/webapp/middleware/index.js +++ b/src/webapp/middleware/index.js @@ -6,13 +6,11 @@ const { sendAction } = ACTIONS; const { BLUETOOTH_CONNECT_REQUEST, BLUETOOTH_SEND_REQUEST, - BLUETOOTH_SYNC_REQUEST, } = TYPES; const REQUESTS = [ BLUETOOTH_CONNECT_REQUEST, BLUETOOTH_SEND_REQUEST, - BLUETOOTH_SYNC_REQUEST, ]; export default (actions = []) => store => next => (action) => {