[ISSUE-11] Notify Message Limit (#12)

* [ISSUE-11] Notify Message Limit

* [ISSUE-11] Unit Testing
This commit is contained in:
jvallelunga 2017-10-26 00:50:42 +02:00 committed by GitHub
parent 3cb1fc853c
commit aa03b16916
19 changed files with 103 additions and 120 deletions

View File

@ -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) => {
<div>
Counter:
<Text {...color}> {counter}</Text>
<br />
<Text>{quote}</Text>
</div>
);
}
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);

View File

@ -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;
}

View File

@ -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",

View File

@ -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(),
};
}

View File

@ -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 (
<div className="app">
{status === 'CONNECTED' &&
<div className={className}>
{store}
<div>
<span className={className}>{counter}</span>
<br />
<span>{quote}</span>
</div>}
<div className="app-actions">
{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,

View File

@ -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,

View File

@ -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;

View File

@ -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',
};

View File

@ -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 };
}

View File

@ -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' });
});

View File

@ -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);
}
};

View File

@ -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');
});

View File

@ -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,
};

View File

@ -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;
});

View File

@ -6,7 +6,6 @@ jest.mock('../central', () => null);
test('actions', () => {
expect(Object.keys(actions)).toEqual([
'connectStore',
'syncStore',
'syncState',
'sendAction',
]);

View File

@ -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;
});

View File

@ -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', () => {

View File

@ -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;

View File

@ -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) => {