├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── example ├── .gitignore ├── Handler.js ├── Root.js ├── main.js ├── package.json ├── public │ └── index.html └── readme.md ├── package.json ├── readme.md ├── src ├── Hash.js ├── History.js ├── actions.js ├── constants.js ├── createMiddleware.js ├── index.js ├── match.js ├── reducer.js └── route.js └── test ├── Hash.js ├── History.js ├── actions.js ├── createMiddleware.js ├── exports.js ├── match.js ├── reducer.js └── route.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": [2, "never"], 5 | "semi": [2, "never"], 6 | "space-before-function-paren": [2, "always"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - npm run check 4 | addons: 5 | apt: 6 | packages: 7 | - xvfb 8 | install: 9 | - export DISPLAY=':99.0' 10 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | - npm install 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Callum Jefferies 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | public/bundle.js 2 | -------------------------------------------------------------------------------- /example/Handler.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { navigate } from 'redux-routing' 3 | 4 | const Handler = props => { 5 | const onNavigate = event => { 6 | event.preventDefault() 7 | props.dispatch(navigate(event.target.href)) 8 | } 9 | 10 | return
11 |

route

12 | 13 |
{JSON.stringify(props.route)}
14 |
15 |
    16 |
  1. /
  2. 17 |
  3. /foo
  4. 18 |
  5. /foo/bar
  6. 19 |
  7. /foo/bar?baz=quux#123
  8. 20 |
  9. /foo/bar/baz
  10. 21 |
  11. /baz
  12. 22 |
23 |
24 | } 25 | 26 | export default Handler 27 | -------------------------------------------------------------------------------- /example/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { match } from 'redux-routing' 4 | 5 | const Root = props => { 6 | const matched = match(props.route.href, props.routes) 7 | 8 | if (matched) { 9 | return 10 | } else { 11 | return
404 not found
12 | } 13 | } 14 | 15 | export default connect(route => ({ route }))(Root) 16 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { applyMiddleware, createStore } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import { createMiddleware, History, navigate, reducer, route } from 'redux-routing' 6 | import Handler from './Handler' 7 | import Root from './Root' 8 | 9 | const routes = [ 10 | route('/', Handler), 11 | route('/foo', Handler), 12 | route('/foo/:bar', Handler) 13 | ] 14 | 15 | const middleware = createMiddleware(History) 16 | const createStoreWithMiddleware = applyMiddleware(middleware)(createStore) 17 | const store = createStoreWithMiddleware(reducer) 18 | 19 | store.dispatch(navigate(window.location.href)) 20 | 21 | ReactDOM.render( 22 | 23 | , document.getElementById('root')) 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "scripts": { 4 | "start": "npm run www & npm run watch", 5 | "watch": "watchify main.js -o public/bundle.js -dv -t [ babelify --presets [ es2015 react ] ]", 6 | "www": "ecstatic -p 8000 public" 7 | }, 8 | "dependencies": { 9 | "react": "^0.14.3", 10 | "react-dom": "^0.14.3", 11 | "react-redux": "^4.0.5", 12 | "redux": "^3.0.5", 13 | "redux-routing": "^0.3.1" 14 | }, 15 | "devDependencies": { 16 | "babel-preset-es2015": "^6.3.13", 17 | "babel-preset-react": "^6.3.13", 18 | "babelify": "^7.2.0", 19 | "ecstatic": "^1.4.0", 20 | "watchify": "^3.6.1" 21 | }, 22 | "license": "public domain" 23 | } 24 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | React application using redux-routing 4 | 5 | ## quick start 6 | 7 | ``` 8 | $ npm install 9 | $ npm start 10 | ``` 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-routing", 3 | "description": "Universal routing built on top of redux", 4 | "main": "lib/index.js", 5 | "jsnext:main": "src/index.js", 6 | "dependencies": { 7 | "path-parser": "^0.3.2" 8 | }, 9 | "devDependencies": { 10 | "babel-cli": "^6.3.17", 11 | "babel-preset-es2015": "^6.3.13", 12 | "babelify": "^7.2.0", 13 | "browserify": "^12.0.1", 14 | "eslint": "^1.10.3", 15 | "eslint-config-airbnb": "^2.1.1", 16 | "eslint-plugin-react": "^3.13.0", 17 | "rimraf": "^2.5.0", 18 | "tape": "^4.3.0", 19 | "tape-run": "^2.1.0" 20 | }, 21 | "scripts": { 22 | "build": "babel src --out-dir lib --presets es2015", 23 | "check": "npm run lint && npm test", 24 | "clean": "rimraf lib", 25 | "lint": "eslint src test", 26 | "prepublish": "npm run clean && npm run build", 27 | "test": "browserify test/*.js -t [ babelify --presets [ es2015 ] ] | tape-run" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/callum/redux-routing" 32 | }, 33 | "keywords": [ 34 | "history", 35 | "react", 36 | "redux", 37 | "router" 38 | ], 39 | "author": "Callum Jefferies (http://callumj.uk/)", 40 | "license": "ISC", 41 | "bugs": { 42 | "url": "https://github.com/callum/redux-routing/issues" 43 | }, 44 | "homepage": "https://github.com/callum/redux-routing", 45 | "version": "0.3.3", 46 | "files": [ 47 | "lib", 48 | "src", 49 | "test" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # redux-routing 2 | 3 | [![Build Status](https://travis-ci.org/callum/redux-routing.svg?branch=master)](https://travis-ci.org/callum/redux-routing) 4 | 5 | Universal routing built on top of [redux](https://github.com/rackt/redux). 6 | 7 | For usage with React see [example/main.js](example/main.js). See [redux-routing-universal-example](https://github.com/callum/redux-routing-universal-example) for an example of a universal application that renders on both client and server. 8 | 9 | ## install 10 | 11 | ``` 12 | npm install redux-routing --save 13 | ``` 14 | 15 | ## how it works 16 | 17 | ```js 18 | import { applyMiddleware, createStore } from 'redux' 19 | import { createMiddleware, History, match, navigate, reducer, route } from 'redux-routing' 20 | 21 | // define routes 22 | const routes = [ 23 | route('/', () => console.log('navigated to /')), 24 | route('/foo', () => console.log('navigated to /foo')), 25 | route('/foo/:bar', () => console.log('navigated to /foo/:bar')) 26 | ] 27 | 28 | // create routing middleware, set up with HTML5 History 29 | const middleware = createMiddleware(History) 30 | 31 | // create store with middleware 32 | const createStoreWithMiddleware = applyMiddleware(middleware)(createStore) 33 | const store = createStoreWithMiddleware(reducer) 34 | 35 | // subscribe to changes 36 | store.subscribe(() => { 37 | const route = store.getState() 38 | const matched = match(route.href, routes) 39 | 40 | if (matched) { 41 | matched.handler() 42 | } else { 43 | console.log('404 not found') 44 | } 45 | }) 46 | 47 | // start navigating 48 | store.dispatch(navigate('/')) 49 | // logs 'navigated to /' 50 | store.dispatch(navigate('/foo')) 51 | // logs 'navigated to /foo' 52 | store.dispatch(navigate('/foo/123')) 53 | // logs 'navigated to /foo/:bar' 54 | store.dispatch(navigate('/foo/bar/baz')) 55 | // logs '404 not found' 56 | ``` 57 | 58 | See [path-parser](https://github.com/troch/path-parser) for more detail on defining routes. 59 | -------------------------------------------------------------------------------- /src/Hash.js: -------------------------------------------------------------------------------- 1 | import { replace } from './actions' 2 | import { NAVIGATE } from './constants' 3 | 4 | function hashToHref (hash) { 5 | return hash.slice(1) || '/' 6 | } 7 | 8 | export default class Hash { 9 | constructor (store) { 10 | this.store = store 11 | } 12 | 13 | listen () { 14 | window.addEventListener('hashchange', () => { 15 | this.onPopHref(hashToHref(window.location.hash)) 16 | }, false) 17 | } 18 | 19 | update (action) { 20 | this.href = action.href 21 | 22 | if (action.type === NAVIGATE) { 23 | this.pushHref(action.href) 24 | } 25 | } 26 | 27 | pushHref (href) { 28 | window.location.hash = href 29 | } 30 | 31 | onPopHref (href) { 32 | if (href !== this.href) { 33 | this.store.dispatch(replace(href)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/History.js: -------------------------------------------------------------------------------- 1 | import { replace } from './actions' 2 | import { NAVIGATE, REPLACE } from './constants' 3 | 4 | export default class History { 5 | constructor (store) { 6 | this.store = store 7 | } 8 | 9 | listen () { 10 | window.addEventListener('popstate', event => { 11 | const { state } = event 12 | 13 | if (typeof state === 'string') { 14 | this.onPopHref(state) 15 | } 16 | }, false) 17 | } 18 | 19 | update (action) { 20 | const href = this.getCurrentHref() 21 | 22 | if (action.type === NAVIGATE) { 23 | if (href && action.href !== href) { 24 | this.pushHref(action.href) 25 | } 26 | } else if (action.type === REPLACE) { 27 | if (href && action.href !== href) { 28 | this.replaceHref(action.href) 29 | } 30 | } 31 | } 32 | 33 | pushHref (href) { 34 | window.history.pushState(href, null, href) 35 | } 36 | 37 | replaceHref (href) { 38 | window.history.replaceState(href, null, href) 39 | } 40 | 41 | onPopHref (href) { 42 | this.store.dispatch(replace(href)) 43 | } 44 | 45 | getCurrentHref () { 46 | return window.history.state 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { NAVIGATE, REPLACE } from './constants' 2 | 3 | export function navigate (href) { 4 | return { type: NAVIGATE, href } 5 | } 6 | 7 | export function replace (href) { 8 | return { type: REPLACE, href } 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const NAVIGATE = '@@redux-routing/navigate' 2 | export const REPLACE = '@@redux-routing/replace' 3 | -------------------------------------------------------------------------------- /src/createMiddleware.js: -------------------------------------------------------------------------------- 1 | import querystring from 'querystring' 2 | import url from 'url' 3 | 4 | export default function createMiddleware (History) { 5 | return store => { 6 | let history 7 | 8 | if (History) { 9 | history = new History(store) 10 | history.listen() 11 | } 12 | 13 | return next => action => { 14 | if (!/^@@redux-routing/.test(action.type)) { 15 | return next(action) 16 | } 17 | 18 | const parsed = url.parse(action.href) 19 | 20 | const location = { 21 | hash: parsed.hash || undefined, 22 | pathname: parsed.pathname, 23 | search: parsed.search || undefined 24 | } 25 | 26 | let query 27 | 28 | if (parsed.query) { 29 | query = querystring.parse(parsed.query) 30 | } 31 | 32 | const result = next(Object.assign({}, action, { 33 | href: url.format(location), 34 | location, 35 | query 36 | })) 37 | 38 | if (history) { 39 | history.update(result) 40 | } 41 | 42 | return result 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createMiddleware from './createMiddleware' 2 | import Hash from './Hash' 3 | import History from './History' 4 | import match from './match' 5 | import reducer from './reducer' 6 | import route from './route' 7 | 8 | export { createMiddleware, Hash, History, match, reducer, route } 9 | export { navigate, replace } from './actions' 10 | -------------------------------------------------------------------------------- /src/match.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | 3 | export default function match (href, routes) { 4 | const parsed = url.parse(href) 5 | 6 | for (const route of routes) { 7 | const matched = route.matcher.match(parsed.href) 8 | 9 | if (matched) { 10 | return { 11 | handler: route.handler, 12 | params: matched, 13 | path: route.path 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { NAVIGATE, REPLACE } from './constants' 2 | 3 | export default function reducer (route = {}, action) { 4 | switch (action.type) { 5 | case NAVIGATE: 6 | case REPLACE: 7 | return { 8 | href: action.href, 9 | location: action.location, 10 | query: action.query 11 | } 12 | 13 | default: 14 | return route 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | import Path from 'path-parser' 2 | 3 | export default function route (path, handler) { 4 | const matcher = new Path(path) 5 | 6 | return { 7 | build: matcher.build.bind(matcher), 8 | matcher, 9 | handler, 10 | path 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/Hash.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { NAVIGATE } from '../src/constants' 3 | import Hash from '../src/Hash' 4 | 5 | test('hashchange listener', t => { 6 | t.plan(1) 7 | 8 | const { addEventListener } = window 9 | 10 | window.addEventListener = event => { 11 | window.addEventListener = addEventListener 12 | t.equal(event, 'hashchange') 13 | } 14 | 15 | const hash = new Hash() 16 | hash.listen() 17 | }) 18 | 19 | test('update state', t => { 20 | t.plan(1) 21 | 22 | const hash = new Hash() 23 | hash.update({ href: '/foo' }) 24 | t.equal(hash.href, '/foo') 25 | }) 26 | 27 | test('push on navigate', t => { 28 | t.plan(1) 29 | 30 | const hash = new Hash() 31 | hash.pushHref = href => t.equal(href, '/foo') 32 | hash.update({ type: NAVIGATE, href: '/foo' }) 33 | }) 34 | 35 | test('on hashchange', t => { 36 | t.plan(1) 37 | 38 | const store = { 39 | dispatch (value) { 40 | t.ok(value) 41 | } 42 | } 43 | 44 | const hash = new Hash(store) 45 | hash.href = '/foo' 46 | 47 | hash.onPopHref('/foo') 48 | hash.onPopHref('/foo/bar') 49 | }) 50 | -------------------------------------------------------------------------------- /test/History.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { NAVIGATE, REPLACE } from '../src/constants' 3 | import History from '../src/History' 4 | 5 | test('popstate listener', t => { 6 | t.plan(1) 7 | 8 | const { addEventListener } = window 9 | 10 | window.addEventListener = event => { 11 | window.addEventListener = addEventListener 12 | t.equal(event, 'popstate') 13 | } 14 | 15 | const history = new History() 16 | history.listen() 17 | }) 18 | 19 | test('push or replace on navigate', t => { 20 | t.plan(2) 21 | 22 | const history = new History() 23 | history.getCurrentHref = () => '/' 24 | history.replaceHref = href => t.equal(href, '/foo') 25 | history.pushHref = href => t.equal(href, '/foo/bar') 26 | 27 | history.update({ type: REPLACE, href: '/foo' }) 28 | history.update({ type: NAVIGATE, href: '/foo/bar' }) 29 | }) 30 | 31 | test('call pushState', t => { 32 | t.plan(3) 33 | 34 | const { pushState } = window.history 35 | 36 | function stub (state, title, href) { 37 | window.history.pushState = pushState 38 | t.equal(href, '/foo') 39 | t.equal(state, '/foo') 40 | t.equal(title, null) 41 | } 42 | 43 | window.history.pushState = stub 44 | 45 | const history = new History() 46 | history.pushHref('/foo') 47 | }) 48 | 49 | test('call replaceState', t => { 50 | t.plan(3) 51 | 52 | const { replaceState } = window.history 53 | 54 | function stub (state, title, href) { 55 | window.history.replaceState = replaceState 56 | t.equal(href, '/foo') 57 | t.equal(state, '/foo') 58 | t.equal(title, null) 59 | } 60 | 61 | window.history.replaceState = stub 62 | 63 | const history = new History() 64 | history.replaceHref('/foo') 65 | }) 66 | 67 | test('on popstate', t => { 68 | t.plan(1) 69 | 70 | const store = { 71 | dispatch (value) { 72 | t.ok(value) 73 | } 74 | } 75 | 76 | const history = new History(store) 77 | history.onPopHref('/foo') 78 | }) 79 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { NAVIGATE, REPLACE } from '../src/constants' 3 | import { navigate, replace } from '../src/actions' 4 | 5 | test('navigate action', t => { 6 | t.plan(1) 7 | t.deepEqual(navigate('foo'), { 8 | href: 'foo', 9 | type: NAVIGATE 10 | }) 11 | }) 12 | 13 | test('replace action', t => { 14 | t.plan(1) 15 | t.deepEqual(replace('foo'), { 16 | href: 'foo', 17 | type: REPLACE 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/createMiddleware.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import createMiddleware from '../src/createMiddleware' 3 | 4 | function noop () { 5 | } 6 | 7 | test('sets up history', t => { 8 | t.plan(2) 9 | 10 | class History { 11 | constructor () { 12 | t.pass() 13 | } 14 | 15 | listen () { 16 | t.pass() 17 | } 18 | } 19 | 20 | const middleware = createMiddleware(History) 21 | middleware() 22 | }) 23 | 24 | test('ignore actions outside of redux-routing', t => { 25 | t.plan(1) 26 | 27 | function next (action) { 28 | t.equal(action, 'foo') 29 | } 30 | 31 | const middleware = createMiddleware() 32 | middleware()(next)('foo') 33 | }) 34 | 35 | test('calling next and returning a value', t => { 36 | t.plan(5) 37 | 38 | function next (result) { 39 | t.pass() 40 | return result 41 | } 42 | 43 | const middleware = createMiddleware() 44 | 45 | const result = middleware()(next)({ 46 | href: '/foo?bar=baz#qux', 47 | type: '@@redux-routing/foo' 48 | }) 49 | 50 | t.deepEqual(result.location, { 51 | hash: '#qux', 52 | pathname: '/foo', 53 | search: '?bar=baz' 54 | }) 55 | t.equal(result.href, '/foo?bar=baz#qux') 56 | t.equal(result.type, '@@redux-routing/foo') 57 | t.deepEqual(result.query, { bar: 'baz' }) 58 | }) 59 | 60 | test('updating history', t => { 61 | t.plan(1) 62 | 63 | class History { 64 | listen () { 65 | } 66 | 67 | update () { 68 | t.pass() 69 | } 70 | } 71 | 72 | const middleware = createMiddleware(History) 73 | 74 | middleware()(noop)({ 75 | href: '/foo', 76 | type: '@@redux-routing/foo' 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /test/exports.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import createMiddleware from '../src/createMiddleware' 3 | import Hash from '../src/Hash' 4 | import History from '../src/History' 5 | import match from '../src/match' 6 | import reducer from '../src/reducer' 7 | import route from '../src/route' 8 | import { navigate, replace } from '../src/actions' 9 | import * as exported from '../src' 10 | 11 | test('exports', t => { 12 | t.plan(8) 13 | t.equal(exported.createMiddleware, createMiddleware) 14 | t.equal(exported.Hash, Hash) 15 | t.equal(exported.History, History) 16 | t.equal(exported.match, match) 17 | t.equal(exported.navigate, navigate) 18 | t.equal(exported.reducer, reducer) 19 | t.equal(exported.replace, replace) 20 | t.equal(exported.route, route) 21 | }) 22 | -------------------------------------------------------------------------------- /test/match.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import Path from 'path-parser' 3 | import match from '../src/match' 4 | 5 | test('no match', t => { 6 | t.plan(1) 7 | t.equal(match('/foo', []), undefined) 8 | }) 9 | 10 | test('a match', t => { 11 | t.plan(1) 12 | 13 | function handler () { 14 | } 15 | 16 | const routes = [ 17 | { 18 | handler, 19 | path: '/foo', 20 | matcher: { 21 | match: () => ({}) 22 | } 23 | } 24 | ] 25 | 26 | const matched = match('/foo', routes) 27 | 28 | t.deepEqual(matched, { 29 | handler, 30 | params: {}, 31 | path: '/foo' 32 | }) 33 | }) 34 | 35 | test('a match with query string parameters', t => { 36 | t.plan(1) 37 | 38 | function handler () { 39 | } 40 | 41 | const path = '/foo?:bar' 42 | 43 | const routes = [ 44 | { 45 | handler, 46 | path, 47 | matcher: new Path(path) 48 | } 49 | ] 50 | 51 | const matched = match('/foo?bar=baz', routes) 52 | 53 | t.deepEqual(matched, { 54 | handler, 55 | params: { bar: 'baz' }, 56 | path: '/foo?:bar' 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/reducer.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import { NAVIGATE, REPLACE } from '../src/constants' 3 | import reducer from '../src/reducer' 4 | 5 | test('initial state', t => { 6 | t.plan(1) 7 | t.deepEqual(reducer(undefined, {}), {}) 8 | }) 9 | 10 | function action (type) { 11 | return { 12 | location: { 13 | hash: 'foo', 14 | pathname: 'bar', 15 | search: 'baz' 16 | }, 17 | href: 'baz', 18 | query: 'bar', 19 | type 20 | } 21 | } 22 | 23 | const expected = { 24 | location: { 25 | hash: 'foo', 26 | pathname: 'bar', 27 | search: 'baz' 28 | }, 29 | href: 'baz', 30 | query: 'bar' 31 | } 32 | 33 | test('handle navigate', t => { 34 | t.plan(1) 35 | t.deepEqual(reducer(undefined, action(NAVIGATE)), expected) 36 | }) 37 | 38 | test('handle replace', t => { 39 | t.plan(1) 40 | t.deepEqual(reducer(undefined, action(REPLACE)), expected) 41 | }) 42 | -------------------------------------------------------------------------------- /test/route.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import route from '../src/route' 3 | 4 | test('creating a route', t => { 5 | t.plan(4) 6 | 7 | const foo = route('/foo', 'bar') 8 | 9 | t.equal(foo.handler, 'bar') 10 | t.equal(foo.path, '/foo') 11 | t.equal(typeof foo.build, 'function') 12 | t.ok(foo.matcher) 13 | }) 14 | --------------------------------------------------------------------------------