├── .babelrc ├── .gitignore ├── .node-version ├── README.md ├── package-lock.json ├── package.json ├── src ├── ButtonLink.js ├── Link.js ├── Location.js ├── Router.js ├── index.js └── withRedirectTo.js ├── test ├── .setup.js └── simple-react-router-test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | 'env', 4 | 'react' 5 | ], 6 | plugins: [ 7 | 'transform-class-properties', 8 | 'transform-object-rest-spread', 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 8.9.4 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-react-router 2 | 3 | [![Join the chat at https://gitter.im/simple-react-router/Lobby](https://badges.gitter.im/simple-react-router/Lobby.svg)](https://gitter.im/simple-react-router/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ## Why 6 | 7 | React Router is too complex. Most of my projects just need a simple top level router. 8 | 9 | ## Usage 10 | 11 | #### Static Routes 12 | 13 | To create a static router simply subclass from `SimpleReactRouter` and define the `routes()` method. 14 | 15 | ```js 16 | import React from 'react' 17 | import SimpleReactRouter from 'simple-react-router' 18 | 19 | // Pages 20 | import NotFound from './components/NotFound' 21 | import HomePage from './components/HomePage' 22 | import SignupPage from './components/SignupPage' 23 | import LoginPage from './components/LoginPage' 24 | import LogoutPage from './components/LogoutPage' 25 | import PostIndexPage from './components/PostIndexPage' 26 | import NewPostPage from './components/NewPostPage' 27 | import PostShowPage from './components/PostShowPage' 28 | import PostEditPage from './components/PostEditPage' 29 | 30 | export default class Router extends SimpleReactRouter { 31 | routes(map){ 32 | map('/', HomePage) 33 | map('/signup', SignupPage) 34 | map('/login', LoginPage) 35 | map('/logout', LogoutPage) 36 | map('/posts', PostIndexPage) 37 | map('/posts/new', NewPostPage) 38 | map('/posts/:postId', PostShowPage) 39 | map('/posts/:postId/edit', PostEditPage) 40 | map('/:path*', NotFound) // catchall route 41 | } 42 | } 43 | ``` 44 | 45 | #### Dynamic Routes 46 | 47 | To use dynamic routes define `getRoute()` instead of `routes()` and you're routes will be calculated every time the `Router` component is constructed or receives props. 48 | 49 | 50 | ```js 51 | import React from 'react' 52 | import SimpleReactRouter from 'simple-react-router' 53 | 54 | // Pages 55 | import NotFound from './components/NotFound' 56 | import HomePage from './components/HomePage' 57 | import SignupPage from './components/SignupPage' 58 | import LoginPage from './components/LoginPage' 59 | import LogoutPage from './components/LogoutPage' 60 | import PostIndexPage from './components/PostIndexPage' 61 | import NewPostPage from './components/NewPostPage' 62 | import PostShowPage from './components/PostShowPage' 63 | import PostEditPage from './components/PostEditPage' 64 | 65 | export default class Router extends SimpleReactRouter { 66 | getRoutes(map, props){ 67 | const { loggedIn } = props 68 | if (loggedIn){ 69 | map('/', LoggedInHomePage) 70 | map('/logout', LogoutPage) 71 | map('/posts/new', NewPostPage) 72 | map('/posts/:postId/edit', PostEditPage) 73 | } else { 74 | map('/', LoggedOutHomePage) 75 | map('/signup', SignupPage) 76 | map('/login', LoginPage) 77 | } 78 | map('/posts', PostIndexPage) 79 | map('/posts/:postId', PostShowPage) 80 | map('/:path*', NotFound) // catchall route 81 | } 82 | } 83 | ``` 84 | 85 | ## Path Expressions 86 | 87 | The route expressions are parsed with [path-to-regexp](https://github.com/pillarjs/path-to-regexp) 88 | via [pathname-router](https://github.com/deadlyicon/pathname-router) 89 | 90 | 91 | ## Links 92 | 93 | 94 | ```js 95 | import { Link } from 'simple-react-router' 96 | 97 | Home 98 | Home 99 | 100 | ``` 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-react-router", 3 | "version": "0.0.17", 4 | "description": "A Simple Router for React", 5 | "repository": "https://github.com/deadlyicon/simple-react-router", 6 | "main": "build/index.js", 7 | "files": [ 8 | "build/index.js", 9 | "build/Link.js", 10 | "build/Location.js", 11 | "build/Router.js" 12 | ], 13 | "scripts": { 14 | "test": "mocha test/.setup.js test/**/*-test.js", 15 | "build": "babel src -d build", 16 | "build:watch": "npm build -- --watch", 17 | "prepublishOnly": "npm run build" 18 | }, 19 | "author": "Jared Grippe ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "babel-cli": "^6.26.0", 23 | "babel-plugin-transform-class-properties": "^6.24.1", 24 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 25 | "babel-preset-env": "^1.6.1", 26 | "babel-preset-react": "^6.24.1", 27 | "chai": "^4.1.2", 28 | "enzyme": "^3.3.0", 29 | "enzyme-adapter-react-16": "^1.1.1", 30 | "jsdom": "^11.6.2", 31 | "mocha": "^5.0.1", 32 | "prop-types": "^15.6.0", 33 | "react": "^16.2.0", 34 | "react-addons-test-utils": "^15.6.2", 35 | "react-dom": "^16.2.0", 36 | "sinon": "^6.3.4", 37 | "sinon-chai": "^3.2.0" 38 | }, 39 | "peerDependencies": { 40 | "react": "^16.2.0", 41 | "prop-types": "^15.6.0" 42 | }, 43 | "dependencies": { 44 | "pathname-router": "0.0.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ButtonLink.js: -------------------------------------------------------------------------------- 1 | import {Link} from './Link'; 2 | 3 | export default function ButtonLink(props){ 4 | return {props.children} 5 | } -------------------------------------------------------------------------------- /src/Link.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Link extends Component { 5 | 6 | static contextTypes = { 7 | redirectTo: PropTypes.func.isRequired, 8 | location: PropTypes.object.isRequired, 9 | }; 10 | 11 | static propTypes = { 12 | Component: PropTypes.node.isRequired, 13 | to: PropTypes.oneOfType([ 14 | PropTypes.string, 15 | PropTypes.object, 16 | ]), 17 | href: PropTypes.string, 18 | path: PropTypes.string, 19 | onClick: PropTypes.func, 20 | externalLink: PropTypes.bool, 21 | }; 22 | 23 | static defaultProps = { 24 | Component: 'a', 25 | externalLink: false, 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | this.onClick = this.onClick.bind(this); 31 | } 32 | 33 | isSameOrigin(){ 34 | const { href } = this.props 35 | return !href.match(/^https?:\/\//) || href.startsWith(window.location.origin) 36 | } 37 | 38 | onClick(event){ 39 | if (this.props.onClick){ 40 | this.props.onClick(event) 41 | } 42 | 43 | if (event.defaultPrevented) return 44 | 45 | if (!this.props.externalLink && !event.ctrlKey && !event.metaKey && !event.shiftKey && this.isSameOrigin()){ 46 | event.preventDefault() 47 | this.context.redirectTo(this.props.href, !!this.props.replace) 48 | } 49 | } 50 | 51 | render(){ 52 | const props = Object.assign({}, this.props) 53 | const Component = props.Component 54 | delete props.Component 55 | delete props.externalLink 56 | props.href = props.href || '' 57 | props.onClick = this.onClick 58 | return { this.link = node }} {...props}>{props.children} 59 | } 60 | } 61 | 62 | export default Link 63 | -------------------------------------------------------------------------------- /src/Location.js: -------------------------------------------------------------------------------- 1 | import querystring from 'querystring' 2 | 3 | export default class Location { 4 | constructor({pathname, query, search, hash}){ 5 | this.pathname = pathname 6 | this.query = typeof search === 'string' 7 | ? searchToObject(search) 8 | : query || {} 9 | this.hash = hash === "" ? null : hash 10 | } 11 | 12 | toString(){ 13 | let href = this.pathname 14 | let query = objectToSearch(this.query) 15 | if (query) href += '?'+query 16 | return href 17 | } 18 | 19 | update({pathname, query, hash}){ 20 | return new Location({ 21 | pathname: pathname || this.pathname, 22 | query: query || this.query, 23 | hash: hash || this.hash, 24 | }) 25 | } 26 | 27 | hrefFor(location) { 28 | return this.update(location).toString() 29 | } 30 | } 31 | 32 | 33 | const searchToObject = (search) => { 34 | return querystring.parse((search || '').replace(/^\?/, '')) 35 | } 36 | 37 | const objectToSearch = (object) => { 38 | return querystring.stringify(object) 39 | } 40 | -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import PathnameRouter from 'pathname-router' 4 | import Location from './Location' 5 | 6 | export default class SimpleReactRouter extends Component { 7 | 8 | static childContextTypes = { 9 | location: PropTypes.object.isRequired, 10 | redirectTo: PropTypes.func.isRequired, 11 | } 12 | 13 | constructor(props){ 14 | super(props) 15 | if (process.env.NODE_ENV === 'development'){ 16 | if (this.routes && this.getRoutes) 17 | throw new Error('you cannot define both routes() and getRoutes()') 18 | if (!this.routes && !this.getRoutes) 19 | throw new Error('you must define either routes() or getRoutes()') 20 | } 21 | 22 | this.router = new Router({ 23 | component: this, 24 | staticRoutes: !!this.routes, 25 | getRoutes: this.routes || this.getRoutes 26 | }) 27 | } 28 | 29 | componentWillUnmount(){ 30 | this.router.unmount() 31 | } 32 | 33 | componentWillReceiveProps(nextProps){ 34 | this.router.update(nextProps) 35 | } 36 | 37 | getChildContext() { 38 | return { 39 | location: this.router.location, 40 | redirectTo: this.router.redirectTo, 41 | } 42 | } 43 | 44 | render(){ 45 | const { router } = this 46 | if (!router.location.params){ 47 | return React.createElement('span', null, 'No Route Found') 48 | } 49 | const { Component } = router.location.params 50 | const props = Object.assign({}, this.props) 51 | props.location = router.location 52 | props.router = router; 53 | return React.createElement(Component, props) 54 | } 55 | } 56 | 57 | 58 | 59 | class Router { 60 | constructor({component, staticRoutes, getRoutes}){ 61 | this.component = component 62 | this.resolve = staticRoutes ? 63 | staticResolver(getRoutes) : 64 | dynamicResolver(getRoutes) 65 | this.rerender = this.rerender.bind(this) 66 | this.redirectTo = this.redirectTo.bind(this) 67 | addEventListener('popstate', this.rerender) 68 | this.update(component.props) 69 | } 70 | 71 | update(props=this.component.props){ 72 | this.location = new Location(window.location) 73 | this.location.params = this.resolve(this.location, props) 74 | this.component.location = this.location 75 | } 76 | 77 | rerender(){ 78 | this.update() 79 | this.component.forceUpdate() 80 | } 81 | 82 | redirectTo(href, replace){ 83 | if (replace){ 84 | window.history.replaceState(null, document.title, href) 85 | }else{ 86 | window.history.pushState(null, document.title, href) 87 | } 88 | this.rerender() 89 | } 90 | 91 | unmount(){ 92 | removeEventListener('popstate', this.rerender) 93 | } 94 | } 95 | 96 | const staticResolver = (mapper) => { 97 | const router = instantiatePathnameRouter(mapper) 98 | return (location) => router.resolve(location.pathname) 99 | } 100 | 101 | const dynamicResolver = (mapper) => { 102 | return (location, props) => 103 | instantiatePathnameRouter(mapper, props).resolve(location.pathname) 104 | } 105 | 106 | const instantiatePathnameRouter = (mapper, props) => { 107 | const router = new PathnameRouter 108 | const map = (path, Component, params={}) => 109 | router.map(path, {Component, ...params}) 110 | mapper.call(null, map, props) 111 | return router 112 | } 113 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Router from './Router' 2 | import Link from './Link' 3 | import withRedirectTo from './withRedirectTo'; 4 | 5 | export { 6 | Router, 7 | Link, 8 | withRedirectTo, 9 | } 10 | 11 | export default Router 12 | -------------------------------------------------------------------------------- /src/withRedirectTo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const withRedirectTo = Component => { 4 | const WithRedirectTo = (props, context) => ( 5 | 6 | ); 7 | 8 | WithRedirectTo.contextTypes = { redirectTo: () => null }; 9 | return WithRedirectTo; 10 | }; 11 | 12 | export default withRedirectTo; 13 | -------------------------------------------------------------------------------- /test/.setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | 3 | const { JSDOM } = require('jsdom'); 4 | const jsdom = new JSDOM(''); 5 | const { window } = jsdom; 6 | 7 | global.jsdom = jsdom; 8 | global.window = window; 9 | global.document = window.document; 10 | global.navigator = { 11 | userAgent: 'node.js', 12 | }; 13 | 14 | Object.getOwnPropertyNames(window) 15 | .filter(prop => typeof global[prop] === 'undefined') 16 | .forEach(prop => { 17 | // if (prop === 'location') return 18 | Object.defineProperties(global, { 19 | [prop]: Object.getOwnPropertyDescriptor(window, prop) 20 | }); 21 | }) 22 | 23 | documentRef = document; 24 | -------------------------------------------------------------------------------- /test/simple-react-router-test.js: -------------------------------------------------------------------------------- 1 | import URL from 'url' 2 | import querystring from 'querystring' 3 | import chai from 'chai' 4 | import sinon from 'sinon' 5 | chai.use(require("sinon-chai")) 6 | const { expect } = chai 7 | import React from 'react' 8 | import ReactDOM from 'react-dom' 9 | import Enzyme, { mount, shallow, render} from 'enzyme'; 10 | import Adapter from 'enzyme-adapter-react-16' 11 | Enzyme.configure({ adapter: new Adapter() }); 12 | 13 | 14 | import SimpleReactRouter from '../src/Router' 15 | import Link from '../src/Link' 16 | 17 | const only = (block) => context.only('', block) 18 | 19 | const setPath = (path) => { 20 | let { pathname, search, hash } = URL.parse(path) 21 | const url = "https://www.example.com"+path 22 | jsdom.reconfigure({url}) 23 | } 24 | 25 | let subject = null 26 | 27 | const setSubject = (subjectGetter) => { 28 | beforeEach(() => { 29 | subject = subjectGetter() 30 | }) 31 | } 32 | 33 | const itShouldBeAReactComponent = (component) => { 34 | it('should be a React Component', () => { 35 | expect(component).to.be.a('function') 36 | }) 37 | } 38 | 39 | const whenAt = (path, block) => { 40 | const url = URL.parse(path) 41 | context(`when at ${path}`, () => { 42 | beforeEach(() => { 43 | setPath(path) 44 | }) 45 | block() 46 | }) 47 | } 48 | 49 | const itShouldRouteTo = (expectedRoute) => { 50 | it(`should route to ${JSON.stringify(expectedRoute)}`, () => { 51 | const { location } = mount(subject).instance().router 52 | expect(location).to.eql(expectedRoute) 53 | }) 54 | } 55 | 56 | const itShouldRender = (Component) => { 57 | it(`should render <${Component.name} />`, () => { 58 | const mountedRouter = mount(subject) 59 | const props = Object.assign({}, mountedRouter.instance().props) 60 | props.location = mountedRouter.instance().router.location 61 | const expectedHTML = render().html() 62 | expect(render(subject).html()).to.eql(expectedHTML) 63 | }) 64 | } 65 | 66 | 67 | 68 | 69 | const NotFound = (props) =>
NotFound {props.location.params.path}
70 | const HomePage = (props) =>
HomePage
71 | const SignupPage = (props) =>
SignupPage
72 | const LoginPage = (props) =>
LoginPage
73 | const LogoutPage = (props) =>
LogoutPage
74 | const PostIndexPage = (props) =>
PostIndexPage
75 | const NewPostPage = (props) =>
NewPostPage
76 | const PostShowPage = (props) =>
PostShowPage
77 | const PostEditPage = (props) =>
PostEditPage
78 | 79 | class StaticRouter extends SimpleReactRouter { 80 | routes(map){ 81 | map('/', HomePage) 82 | map('/signup', SignupPage) 83 | map('/login', LoginPage) 84 | map('/logout', LogoutPage) 85 | map('/posts', PostIndexPage) 86 | map('/posts/new', NewPostPage) 87 | map('/posts/:postId', PostShowPage) 88 | map('/posts/:postId/edit', PostEditPage) 89 | map('/:path*', NotFound) // catchall route 90 | } 91 | } 92 | 93 | 94 | describe('StaticRouter', () => { 95 | 96 | setSubject(() => ) 97 | 98 | itShouldBeAReactComponent(StaticRouter) 99 | 100 | whenAt('/', () => { 101 | itShouldRouteTo({ 102 | pathname: '/', 103 | query: {}, 104 | hash: null, 105 | params: { 106 | Component: HomePage, 107 | }, 108 | }) 109 | 110 | itShouldRender(HomePage) 111 | 112 | }) 113 | 114 | whenAt('/signup', () => { 115 | 116 | itShouldRouteTo({ 117 | pathname: '/signup', 118 | query: {}, 119 | hash: null, 120 | params: { 121 | Component: SignupPage, 122 | }, 123 | }) 124 | 125 | itShouldRender(SignupPage) 126 | 127 | }) 128 | 129 | whenAt('/login?return=/about#pricing', () => { 130 | 131 | itShouldRouteTo({ 132 | pathname: '/login', 133 | query: { 134 | "return": "/about" 135 | }, 136 | hash: "#pricing", 137 | params: { 138 | Component: LoginPage, 139 | }, 140 | }) 141 | 142 | itShouldRender(LoginPage) 143 | 144 | }) 145 | 146 | whenAt('/posts/42', () => { 147 | 148 | itShouldRouteTo({ 149 | pathname: '/posts/42', 150 | query: {}, 151 | hash: null, 152 | params: { 153 | Component: PostShowPage, 154 | postId: "42", 155 | }, 156 | }) 157 | 158 | itShouldRender(PostShowPage) 159 | 160 | }) 161 | 162 | whenAt('/posts/88/edit', () => { 163 | 164 | itShouldRouteTo({ 165 | pathname: '/posts/88/edit', 166 | query: {}, 167 | hash: null, 168 | params: { 169 | Component: PostEditPage, 170 | postId: "88", 171 | }, 172 | }) 173 | 174 | itShouldRender(PostEditPage) 175 | 176 | }) 177 | 178 | whenAt('/some/unknown/path', () => { 179 | 180 | itShouldRouteTo({ 181 | pathname: '/some/unknown/path', 182 | query: {}, 183 | hash: null, 184 | params: { 185 | Component: NotFound, 186 | path: 'some/unknown/path', 187 | }, 188 | }) 189 | 190 | itShouldRender(NotFound) 191 | 192 | }) 193 | 194 | 195 | }) 196 | 197 | 198 | 199 | 200 | 201 | 202 | const LoggedInHomePage = (props) =>
LoggedInHomePage
203 | const LoggedOutHomePage = (props) =>
LoggedOutHomePage
204 | 205 | class DynamicRouter extends SimpleReactRouter { 206 | getRoutes(map, props){ 207 | const { loggedIn } = props 208 | if (loggedIn){ 209 | map('/', LoggedInHomePage) 210 | map('/logout', LogoutPage) 211 | map('/posts/new', NewPostPage) 212 | map('/posts/:postId/edit', PostEditPage) 213 | } else { 214 | map('/', LoggedOutHomePage) 215 | map('/signup', SignupPage) 216 | map('/login', LoginPage) 217 | } 218 | map('/posts', PostIndexPage) 219 | map('/posts/:postId', PostShowPage) 220 | map('/:path*', NotFound) // catchall route 221 | } 222 | } 223 | 224 | 225 | describe('DynamicRouter', () => { 226 | 227 | context('when not logged in', () => { 228 | setSubject(() => ) 229 | 230 | whenAt('/', () => { 231 | 232 | itShouldRouteTo({ 233 | pathname: '/', 234 | query: {}, 235 | hash: null, 236 | params: { 237 | Component: LoggedOutHomePage, 238 | }, 239 | }) 240 | 241 | itShouldRender(LoggedOutHomePage) 242 | 243 | }) 244 | 245 | whenAt('/signup', () => { 246 | 247 | itShouldRouteTo({ 248 | pathname: '/signup', 249 | query: {}, 250 | hash: null, 251 | params: { 252 | Component: SignupPage, 253 | }, 254 | }) 255 | 256 | itShouldRender(SignupPage) 257 | 258 | }) 259 | 260 | whenAt('/login', () => { 261 | 262 | itShouldRouteTo({ 263 | pathname: '/login', 264 | query: {}, 265 | hash: null, 266 | params: { 267 | Component: LoginPage, 268 | }, 269 | }) 270 | 271 | itShouldRender(LoginPage) 272 | 273 | }) 274 | 275 | whenAt('/posts', () => { 276 | 277 | itShouldRouteTo({ 278 | pathname: '/posts', 279 | query: {}, 280 | hash: null, 281 | params: { 282 | Component: PostIndexPage, 283 | }, 284 | }) 285 | 286 | itShouldRender(PostIndexPage) 287 | 288 | }) 289 | 290 | whenAt('/posts/88/edit', () => { 291 | 292 | itShouldRouteTo({ 293 | pathname: '/posts/88/edit', 294 | query: {}, 295 | hash: null, 296 | params: { 297 | Component: NotFound, 298 | path: 'posts/88/edit' 299 | }, 300 | }) 301 | 302 | itShouldRender(NotFound) 303 | 304 | }) 305 | 306 | 307 | whenAt('/posts/42', () => { 308 | 309 | itShouldRouteTo({ 310 | pathname: '/posts/42', 311 | query: {}, 312 | hash: null, 313 | params: { 314 | Component: PostShowPage, 315 | postId: "42", 316 | }, 317 | }) 318 | 319 | itShouldRender(PostShowPage) 320 | 321 | }) 322 | 323 | 324 | whenAt('/some/unknown/path', () => { 325 | 326 | itShouldRouteTo({ 327 | pathname: '/some/unknown/path', 328 | query: {}, 329 | hash: null, 330 | params: { 331 | Component: NotFound, 332 | path: 'some/unknown/path', 333 | }, 334 | }) 335 | 336 | itShouldRender(NotFound) 337 | 338 | }) 339 | 340 | }) 341 | 342 | context('when logged in', () => { 343 | setSubject(() => ) 344 | 345 | whenAt('/', () => { 346 | 347 | itShouldRouteTo({ 348 | pathname: '/', 349 | query: {}, 350 | hash: null, 351 | params: { 352 | Component: LoggedInHomePage, 353 | }, 354 | }) 355 | 356 | itShouldRender(LoggedInHomePage) 357 | 358 | }) 359 | 360 | whenAt('/signup', () => { 361 | 362 | itShouldRouteTo({ 363 | pathname: '/signup', 364 | query: {}, 365 | hash: null, 366 | params: { 367 | Component: NotFound, 368 | path: 'signup', 369 | } 370 | }) 371 | 372 | itShouldRender(NotFound) 373 | 374 | }) 375 | 376 | whenAt('/login', () => { 377 | 378 | itShouldRouteTo({ 379 | pathname: '/login', 380 | query: {}, 381 | hash: null, 382 | params: { 383 | Component: NotFound, 384 | path: 'login' 385 | }, 386 | }) 387 | 388 | itShouldRender(NotFound) 389 | 390 | }) 391 | 392 | whenAt('/posts', () => { 393 | 394 | itShouldRouteTo({ 395 | pathname: '/posts', 396 | query: {}, 397 | hash: null, 398 | params: { 399 | Component: PostIndexPage, 400 | }, 401 | }) 402 | 403 | itShouldRender(PostIndexPage) 404 | 405 | }) 406 | 407 | whenAt('/posts/88/edit', () => { 408 | 409 | itShouldRouteTo({ 410 | pathname: '/posts/88/edit', 411 | query: {}, 412 | hash: null, 413 | params: { 414 | Component: PostEditPage, 415 | postId: '88' 416 | }, 417 | }) 418 | 419 | itShouldRender(PostEditPage) 420 | 421 | }) 422 | 423 | 424 | whenAt('/posts/42', () => { 425 | 426 | itShouldRouteTo({ 427 | pathname: '/posts/42', 428 | query: {}, 429 | hash: null, 430 | params: { 431 | Component: PostShowPage, 432 | postId: "42", 433 | }, 434 | }) 435 | 436 | itShouldRender(PostShowPage) 437 | 438 | }) 439 | 440 | 441 | whenAt('/some/unknown/path', () => { 442 | 443 | itShouldRouteTo({ 444 | pathname: '/some/unknown/path', 445 | query: {}, 446 | hash: null, 447 | params: { 448 | Component: NotFound, 449 | path: 'some/unknown/path', 450 | }, 451 | }) 452 | 453 | itShouldRender(NotFound) 454 | 455 | }) 456 | }) 457 | 458 | }) 459 | 460 | describe('Location', () => { 461 | 462 | let location 463 | beforeEach(() => { 464 | setPath('/posts/23/edit?order=desc') 465 | const mountPoint = mount() 466 | location = mountPoint.instance().router.location 467 | }) 468 | 469 | it('should', ()=> { 470 | expect(location).to.eql({ 471 | pathname: '/posts/23/edit', 472 | query: { 473 | "order": "desc", 474 | }, 475 | hash: null, 476 | params: { 477 | Component: PostEditPage, 478 | postId: '23' 479 | }, 480 | }) 481 | 482 | expect(location.toString()).to.eql('/posts/23/edit?order=desc') 483 | 484 | expect(location.hrefFor({})).to.eql('/posts/23/edit?order=desc') 485 | 486 | expect(location.hrefFor({ 487 | pathname: '/foo/bar' 488 | })).to.eql('/foo/bar?order=desc') 489 | 490 | expect(location.hrefFor({ 491 | pathname: '/foo/bar', 492 | query: {order: 'asc'}, 493 | })).to.eql('/foo/bar?order=asc') 494 | 495 | }) 496 | 497 | }) 498 | 499 | describe('Link', () => { 500 | 501 | class LinkTestRouter extends SimpleReactRouter { 502 | routes(map){ 503 | map('/a', () =>
) 504 | map('/b', () =>
) 505 | map('/c', () =>
) 506 | map('/d', () =>
) 507 | map('/e', () =>
) 508 | } 509 | } 510 | 511 | let replaceStateSpy, pushStateSpy 512 | 513 | const theLinkShouldUseTheRouter = () => { 514 | it(`should use the router`, () => { 515 | const link = mount(subject).find('a') 516 | link.simulate('click') 517 | expect(replaceStateSpy).to.have.not.been.called 518 | expect(pushStateSpy).to.have.been.calledWith(null, '', link.props().href) 519 | }) 520 | } 521 | 522 | const theLinkShouldNotPreventDefault = () => { 523 | it(`should use the router`, () => { 524 | mount(subject).find('a').simulate('click') 525 | expect(pushStateSpy).to.have.not.been.called 526 | expect(replaceStateSpy).to.have.not.been.called 527 | }) 528 | } 529 | 530 | 531 | beforeEach(function(){ 532 | replaceStateSpy = sinon.spy(window.history, 'replaceState') 533 | pushStateSpy = sinon.spy(window.history, 'pushState') 534 | }) 535 | 536 | afterEach(function(){ 537 | replaceStateSpy.restore() 538 | pushStateSpy.restore() 539 | }) 540 | 541 | setSubject(() => ) 542 | 543 | whenAt('/a', function(){ 544 | theLinkShouldUseTheRouter() 545 | }) 546 | 547 | whenAt('/b', function(){ 548 | theLinkShouldUseTheRouter() 549 | }) 550 | 551 | whenAt('/c', function(){ 552 | theLinkShouldNotPreventDefault() 553 | }) 554 | 555 | whenAt('/d', function(){ 556 | theLinkShouldNotPreventDefault() 557 | }) 558 | 559 | whenAt('/e', function(){ 560 | it('should be able to be rendered as a