[ISSUE-11] Notify Message Limit (#12)
* [ISSUE-11] Notify Message Limit * [ISSUE-11] Unit Testing
This commit is contained in:
parent
3cb1fc853c
commit
aa03b16916
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ jest.mock('../central', () => null);
|
|||
test('actions', () => {
|
||||
expect(Object.keys(actions)).toEqual([
|
||||
'connectStore',
|
||||
'syncStore',
|
||||
'syncState',
|
||||
'sendAction',
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue