diff --git a/package.json b/package.json index 91b4c3c..0139322 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "precommit": "npm run lint" }, "dependencies": { - "antd": "^2.6.4", + "antd": "^2.8.3", "babel-runtime": "^6.22.0", "dva": "^1.2.0", "dva-loading": "^0.2.0", diff --git a/src/components/Logger/Logger.js b/src/components/Logger/Logger.js new file mode 100644 index 0000000..544e603 --- /dev/null +++ b/src/components/Logger/Logger.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { connect } from 'dva'; + +function Logger({ logs }) { + return ( +
+
+

Logger

+ +
+ ); +} + +Logger.propTypes = { +}; + +function mapStateToProps(state) { + const { logs } = state.logger; + return { + logs, + }; +} +export default connect(mapStateToProps)(Logger); diff --git a/src/components/MainLayout/Header.js b/src/components/MainLayout/Header.js index e247711..be1aa6f 100644 --- a/src/components/MainLayout/Header.js +++ b/src/components/MainLayout/Header.js @@ -15,10 +15,19 @@ function Header({ location }) { Home + + Tasks + + + A + + + B + 404 - + dva diff --git a/src/components/Reusable/Reusable.js b/src/components/Reusable/Reusable.js new file mode 100644 index 0000000..156850d --- /dev/null +++ b/src/components/Reusable/Reusable.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { connect } from 'dva'; + +function Reusable({ messages }) { + return ( +
+
+

Messages

+ +
+ ); +} + +Reusable.propTypes = { +}; + +function mapStateToProps(state) { + const { messages } = state.reusable; + return { + messages, + }; +} +export default connect(mapStateToProps)(Reusable); diff --git a/src/index.js b/src/index.js index ec545b9..653003e 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,9 @@ import { message } from 'antd'; import './index.html'; import './index.css'; +import logger from './models/logger'; +import reusable from './models/reusable'; + const ERROR_MSG_DURATION = 3; // 3 秒 // 1. Initialize @@ -20,6 +23,8 @@ app.use(createLoading()); // 3. Model // Moved to router.js +app.model(logger); +app.model(reusable); // 4. Router app.router(require('./router')); diff --git a/src/models/a.js b/src/models/a.js new file mode 100644 index 0000000..9d2e606 --- /dev/null +++ b/src/models/a.js @@ -0,0 +1,15 @@ +import { delay } from '../services/delay'; + +export default { + namespace: 'a', + state: '', + reducers: { + }, + effects: { + *foo(action, { call, put }) { + yield call(delay, 1000); + yield put({ type: 'reusable/addMessage', payload: '666' }); + }, + }, +}; + diff --git a/src/models/b.js b/src/models/b.js new file mode 100644 index 0000000..5a84177 --- /dev/null +++ b/src/models/b.js @@ -0,0 +1,28 @@ +import { delay } from '../services/delay'; + +export default { + namespace: 'b', + state: '', + reducers: { + }, + effects: { + addWatcher: [function *watcher({ take, put }) { + /* eslint-disable */ + while (true) { + /* eslint-enable */ + // watch foo action from model a + const data1 = yield take('a/foo'); + yield put({ type: 'reusable/addMessage', payload: `observe an action from model A: ${data1}` }); + + // watch addLogSuccess action from model reuseable + const data2 = yield take('reusable/addMessageSuccess'); + yield put({ type: 'reusable/addMessage', payload: `observe an action from model reusable: ${data2}` }); + } + }, { type: 'watcher' }], + + *foo(action, { call, put }) { + yield call(delay, 3000); + yield put({ type: 'reusable/addMessage', payload: '233' }); + }, + }, +}; diff --git a/src/models/logger.js b/src/models/logger.js new file mode 100644 index 0000000..b0c9b28 --- /dev/null +++ b/src/models/logger.js @@ -0,0 +1,21 @@ +export default { + namespace: 'logger', + state: { + logs: [], + }, + reducers: { + addLog(state, { payload }) { + const log = { + id: state.logs.length, + text: payload, + }; + + const logs = state.logs.concat([log]); + return { + logs, + }; + }, + }, + effects: { + }, +}; diff --git a/src/models/reusable.js b/src/models/reusable.js new file mode 100644 index 0000000..49c7a18 --- /dev/null +++ b/src/models/reusable.js @@ -0,0 +1,34 @@ +import { delay } from '../services/delay'; + +export default { + namespace: 'reusable', + state: { + messages: [], + }, + reducers: { + addMessageSuccess(state, { payload }) { + const message = { + id: state.messages.length, + text: payload, + }; + + const messages = state.messages.concat([message]); + return { + messages, + }; + }, + }, + effects: { + *addMessage(action, { call, put }) { + const { payload } = action; + const { resolve } = payload; + yield call(delay, 1000); + + if (resolve) { + resolve(payload); + } + + yield put({ type: 'addMessageSuccess', payload }); + }, + }, +}; diff --git a/src/models/task.js b/src/models/task.js new file mode 100644 index 0000000..5f044ab --- /dev/null +++ b/src/models/task.js @@ -0,0 +1,44 @@ +import { delay } from '../services/delay'; +import { makeService } from '../services/makeService'; + +export default { + namespace: 'task', + state: { + }, + reducers: { + }, + effects: { + *sequential(action, { call, put }) { + yield call(delay, 3000); + yield put({ type: 'logger/addLog', payload: 'step1' }); + yield call(delay, 5000); + yield put({ type: 'logger/addLog', payload: 'step2' }); + }, + *parallel(action, { call, put }) { + yield put({ type: 'logger/addLog', payload: 'all started' }); + + // 注意这里不要写yield*,不然会顺序执行的 + yield [ + call(makeService(3000), 'service 1'), + call(makeService(5000), 'service 2'), + call(makeService(7000), 'service 3'), + ]; + + yield put({ type: 'logger/addLog', payload: 'all completed' }); + }, + *race(action, { call, put, race }) { + yield put({ type: 'logger/addLog', payload: 'start race' }); + + const { data, timeout } = yield race({ + data: call(makeService(2000), 'some data'), + timeout: call(delay, 3000), + }); + + if (data) { + yield put({ type: 'logger/addLog', payload: data }); + } else { + yield put({ type: 'logger/addLog', payload: `timeout: ${timeout}` }); + } + }, + }, +}; diff --git a/src/models/users.js b/src/models/users.js index 3914c5d..a066b6d 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -1,6 +1,9 @@ import * as usersService from '../services/users'; -export default { +// selector放在这里,而且要export出来,不然不好写测试…… +export const pageSelector = state => state.users.page; + +export const user = { namespace: 'users', state: { list: [], @@ -37,7 +40,7 @@ export default { yield put({ type: 'reload' }); }, *reload(action, { put, select }) { - const page = yield select(state => state.users.page); + const page = yield select(pageSelector); yield put({ type: 'fetch', payload: { page } }); }, }, diff --git a/src/router.js b/src/router.js index d86e921..dfd6525 100644 --- a/src/router.js +++ b/src/router.js @@ -25,11 +25,41 @@ function RouterConfig({ history, app }) { name: 'UsersPage', getComponent(nextState, cb) { require.ensure([], (require) => { - registerModel(app, require('./models/users')); + registerModel(app, require('./models/users').user); cb(null, require('./routes/Users')); }); }, }, + { + path: '/tasks', + name: 'TasksPage', + getComponent(nextState, cb) { + require.ensure([], (require) => { + cb(null, require('./routes/Tasks')); + registerModel(app, require('./models/task')); + }); + }, + }, + { + path: '/a', + name: 'APage', + getComponent(nextState, cb) { + require.ensure([], (require) => { + cb(null, require('./routes/A')); + registerModel(app, require('./models/a')); + }); + }, + }, + { + path: '/b', + name: 'BPage', + getComponent(nextState, cb) { + require.ensure([], (require) => { + cb(null, require('./routes/B')); + registerModel(app, require('./models/b')); + }); + }, + }, ]; return ; diff --git a/src/routes/A.js b/src/routes/A.js new file mode 100644 index 0000000..8ac2d33 --- /dev/null +++ b/src/routes/A.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'dva'; +import MainLayout from '../components/MainLayout/MainLayout'; +import Reusable from '../components/Reusable/Reusable'; +import Logger from '../components/Logger/Logger'; + +function A({ dispatch, location }) { + const bar = () => { + new Promise((resolve, reject) => { + dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } }); + }) + .then(() => { + // console.log(`after a long time, ${data} returns`); + }); + }; + + return ( + +

Component A

+
+ + + + +
+
+ ); +} + +A.propTypes = { +}; + +export default connect()(A); diff --git a/src/routes/B.js b/src/routes/B.js new file mode 100644 index 0000000..c267c85 --- /dev/null +++ b/src/routes/B.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { connect } from 'dva'; +import MainLayout from '../components/MainLayout/MainLayout'; +import Reusable from '../components/Reusable/Reusable'; +import Logger from '../components/Logger/Logger'; + +function B({ dispatch, location }) { + return ( + +

Component B

+
+ + + +
+
+ ); +} + +B.propTypes = { +}; + +export default connect()(B); diff --git a/src/routes/Tasks.js b/src/routes/Tasks.js new file mode 100644 index 0000000..d4a0793 --- /dev/null +++ b/src/routes/Tasks.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { connect } from 'dva'; +import MainLayout from '../components/MainLayout/MainLayout'; +import Logger from '../components/Logger/Logger'; + +function Tasks({ dispatch, location }) { + return ( + +
+ + + +
+ +
+ ); +} + +Tasks.propTypes = { +}; + +export default connect()(Tasks); diff --git a/src/services/delay.js b/src/services/delay.js new file mode 100644 index 0000000..09b2154 --- /dev/null +++ b/src/services/delay.js @@ -0,0 +1,5 @@ +export const delay = (timeout) => { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +}; diff --git a/src/services/makeService.js b/src/services/makeService.js new file mode 100644 index 0000000..c9783d3 --- /dev/null +++ b/src/services/makeService.js @@ -0,0 +1,12 @@ +export const makeService = (time) => { + return (data) => { + // console.log(`${data} start`); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(data); + // console.log(`${data} end`); + }, time); + }); + }; +}; diff --git a/test/a.spec.js b/test/a.spec.js new file mode 100644 index 0000000..d1bf0ea --- /dev/null +++ b/test/a.spec.js @@ -0,0 +1,26 @@ +import expect from 'expect'; +import { effects } from 'dva/saga'; +import { delay } from '../src/services/delay'; +import a from '../src/models/a'; + +describe('A Model', () => { + it('loads', () => { + expect(a).toExist(); + }); + + describe('effects', () => { + it('foo should work', () => { + const { call, put } = effects; + + const sagas = a.effects; + const saga = sagas.foo; + const generator = saga({ type: 'a/foo' }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(delay, 1000)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'reusable/addMessage', payload: '666' })); + }); + }); +}); diff --git a/test/b.spec.js b/test/b.spec.js new file mode 100644 index 0000000..600f346 --- /dev/null +++ b/test/b.spec.js @@ -0,0 +1,26 @@ +import expect from 'expect'; +import { effects } from 'dva/saga'; +import { delay } from '../src/services/delay'; +import b from '../src/models/b'; + +describe('B Model', () => { + it('loads', () => { + expect(b).toExist(); + }); + + describe('effects', () => { + it('foo should work', () => { + const { call, put } = effects; + + const sagas = b.effects; + const saga = sagas.foo; + const generator = saga({ type: 'b/foo' }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(delay, 3000)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'reusable/addMessage', payload: '233' })); + }); + }); +}); diff --git a/test/logger.spec.js b/test/logger.spec.js new file mode 100644 index 0000000..70ceaba --- /dev/null +++ b/test/logger.spec.js @@ -0,0 +1,19 @@ +import expect from 'expect'; +import logger from '../src/models/logger'; + +describe('Logger Model', () => { + it('loads', () => { + expect(logger).toExist(); + }); + + describe('reducers', () => { + it('addLog should work', () => { + const reducers = logger.reducers; + const reducer = reducers.addLog; + const state = { + logs: [], + }; + expect(reducer(state, { payload: 'log' })).toEqual({ logs: [{ id: 0, text: 'log' }] }); + }); + }); +}); diff --git a/test/reusable.spec.js b/test/reusable.spec.js new file mode 100644 index 0000000..6f37194 --- /dev/null +++ b/test/reusable.spec.js @@ -0,0 +1,37 @@ +import expect from 'expect'; +import { effects } from 'dva/saga'; +import { delay } from '../src/services/delay'; +import reusable from '../src/models/reusable'; + +describe('Reusable Model', () => { + it('loads', () => { + expect(reusable).toExist(); + }); + + describe('reducers', () => { + it('addMessageSuccess should work', () => { + const reducers = reusable.reducers; + const reducer = reducers.addMessageSuccess; + const state = { + messages: [], + }; + expect(reducer(state, { payload: 'message' })).toEqual({ messages: [{ id: 0, text: 'message' }] }); + }); + }); + + describe('effects', () => { + it('addMessage should work', () => { + const { call, put } = effects; + + const sagas = reusable.effects; + const saga = sagas.addMessage; + const generator = saga({ type: 'reusable/addMessage', payload: '666' }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(delay, 1000)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'addMessageSuccess', payload: '666' })); + }); + }); +}); diff --git a/test/task.spec.js b/test/task.spec.js new file mode 100644 index 0000000..5beb6a2 --- /dev/null +++ b/test/task.spec.js @@ -0,0 +1,80 @@ +import expect from 'expect'; +import { effects } from 'dva/saga'; + +import { delay } from '../src/services/delay'; +import { makeService } from '../src/services/makeService'; + +import task from '../src/models/task'; + +describe('Task Model', () => { + it('loads', () => { + expect(task).toExist(); + }); + + describe('effects', () => { + it('sequential should work', () => { + const { call, put } = effects; + + const sagas = task.effects; + const saga = sagas.sequential; + const generator = saga({ type: 'task/sequential' }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(delay, 3000)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'step1' })); + + next = generator.next(); + expect(next.value).toEqual(call(delay, 5000)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'step2' })); + }); + + it('parallel should work', () => { + const { call, put } = effects; + + const sagas = task.effects; + const saga = sagas.parallel; + const generator = saga({ type: 'task/parallel' }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'all started' })); + + next = generator.next(); + expect(next.value).toEqual([ + call(makeService(3000), 'service 1'), + call(makeService(5000), 'service 2'), + call(makeService(7000), 'service 3'), + ]); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'all completed' })); + }); + + it('race shold work', () => { + const { call, put, race } = effects; + + const sagas = task.effects; + const saga = sagas.race; + const generator = saga({ type: 'task/race' }, { call, put, race }); + + let next = generator.next(); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: 'start race' })); + + next = generator.next(); + expect(next.value).toEqual(race({ + data: call(makeService(2000), 'some data'), + timeout: call(delay, 3000), + })); + + // 因为在单元测试里,跑不了被测逻辑的那个if + // 那个if是依赖于实际执行逻辑的,但saga的单元测试只是测action的序列化结果是否一致,并不跑真实逻辑 + // 所以要手动注入一个结果进去 + const data = 'some data'; + next = generator.next({ data }); + expect(next.value).toEqual(put({ type: 'logger/addLog', payload: data })); + }); + }); +}); diff --git a/test/user.spec.js b/test/user.spec.js new file mode 100644 index 0000000..768a96d --- /dev/null +++ b/test/user.spec.js @@ -0,0 +1,143 @@ +import expect from 'expect'; +import { effects } from 'dva/saga'; + +import * as usersService from '../src/services/users'; + +import { user, pageSelector } from '../src/models/users'; + +describe('User Model', () => { + it('loads', () => { + expect(user).toExist(); + }); + + describe('reducers', () => { + it('save should work', () => { + const reducers = user.reducers; + const reducer = reducers.save; + const state = { + list: [], + total: null, + page: null, + }; + + const action = { + type: 'save', + payload: { + data: [{ id: '01' }, { id: '02' }], + total: 111, + page: 1, + }, + }; + + expect(reducer(state, action)).toEqual({ + list: [{ id: '01' }, { id: '02' }], + total: 111, + page: 1, + }); + }); + }); + + describe('effects', () => { + it('fetch should work', () => { + const { call, put } = effects; + + const sagas = user.effects; + const saga = sagas.fetch; + + const generator = saga({ type: 'fetch', payload: { page: 1 } }, { call, put }); + + let next = generator.next(); + const page = 1; + expect(next.value).toEqual(call(usersService.fetch, { page })); + + next = generator.next({ data: [], + headers: { + 'x-total-count': '111', + }, + }); + + expect(next.value).toEqual(put({ + type: 'save', + payload: { + data: [], + total: 111, + page: 1, + }, + })); + }); + + it('remove should work', () => { + const { call, put } = effects; + + const sagas = user.effects; + const saga = sagas.remove; + + const id = 'id111'; + + const generator = saga({ type: 'remove', payload: id }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(usersService.remove, id)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'reload' })); + }); + + it('patch should work', () => { + const { call, put } = effects; + + const sagas = user.effects; + const saga = sagas.patch; + + const id = 'id111'; + const values = []; + + const generator = saga({ type: 'patch', payload: { id, values } }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(usersService.patch, id, values)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'reload' })); + }); + + it('create should work', () => { + const { call, put } = effects; + + const sagas = user.effects; + const saga = sagas.create; + + const values = []; + + const generator = saga({ type: 'create', payload: values }, { call, put }); + + let next = generator.next(); + expect(next.value).toEqual(call(usersService.create, values)); + + next = generator.next(); + expect(next.value).toEqual(put({ type: 'reload' })); + }); + + it('reload should work', () => { + const { select, put } = effects; + + const sagas = user.effects; + const saga = sagas.reload; + + const generator = saga({ type: 'reload' }, { select, put }); + + const fakeState = { + users: { + page: 1, + }, + }; + let next = generator.next(fakeState); + // 下面这句的select,源码和测试两边需要是同一个selector函数才可以…… + // 然而,如果源model里面不把这个selector命名并且export出来,这里肯定过不了 + expect(next.value).toEqual(select(pageSelector)); + + next = generator.next(2); + expect(next.value).toEqual(put({ type: 'fetch', payload: { page: 2 } })); + }); + }); +});