Skip to content
This repository has been archived by the owner on Mar 10, 2021. It is now read-only.

Commit

Permalink
Progress, progress, progress...
Browse files Browse the repository at this point in the history
  • Loading branch information
bkonkle committed Nov 24, 2015
1 parent e87736e commit 594e061
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 60 deletions.
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -28,7 +28,8 @@
},
"dependencies": {
"curry": "^1.2.0",
"redux-actions": "^0.8.0"
"redux-actions": "^0.8.0",
"uniloc": "^0.2.0"
},
"devDependencies": {
"babel-cli": "^6.2.0",
Expand All @@ -43,6 +44,7 @@
"eslint-config-ecliptic": "^1.3.0",
"jsx-chai": "^1.1.1",
"mocha": "^2.3.3",
"proxyquire": "^1.7.3",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0"
}
Expand Down
5 changes: 2 additions & 3 deletions src/actions.js
@@ -1,6 +1,5 @@
import {createAction} from 'redux-actions'
import {INIT_ROUTER, URL_CHANGED} from './index'

export const initRouter = createAction(INIT_ROUTER)
import {URL_CHANGED, NAVIGATE} from './index'

export const urlChanged = createAction(URL_CHANGED)
export const navigate = createAction(NAVIGATE)
1 change: 1 addition & 0 deletions src/index.js
@@ -1,2 +1,3 @@
export const INIT_ROUTER = 'INIT_ROUTER'
export const URL_CHANGED = 'URL_CHANGED'
export const NAVIGATE = 'NAVIGATE'
22 changes: 10 additions & 12 deletions src/init.js
@@ -1,19 +1,17 @@
import * as actions from './actions'
import {configureRouter} from './router'
import {getUrl} from './utils'
import {urlChanged} from './actions'

export function getUrl(universal = false, store) {
if (universal) {
return store.getState().router.url
}
return window.location.pathname + window.location.search
}

export function handlePopState(store) {
export const handlePopState = store => () => {
const url = getUrl()
store.dispatch(actions.urlChanged({url, source: 'popState'}))
store.dispatch(urlChanged({url, source: 'popState'}))
}

export default function init(store, routes, aliases) {
const url = getUrl()
store.dispatch(actions.initRouter({url, routes, aliases}))
window.onpopstate = handlePopState

configureRouter(routes, aliases)

store.dispatch(urlChanged({url, source: 'init'}))
window.onpopstate = handlePopState(store)
}
25 changes: 21 additions & 4 deletions src/reducer.js
@@ -1,9 +1,26 @@
import {URL_CHANGED} from '../src/index'
import handleActions from 'redux-actions'
import uniloc from 'uniloc'
import {getUrl} from './utils'
import {handleActions} from 'redux-actions'
import {URL_CHANGED, NAVIGATE} from '../src/index'
import router from '../src/router'

export function routeState(url) {
return {route: router.lookup(url), url}
}

export default handleActions({
[URL_CHANGED]: (state, action) => {

return Object.assign({}, state, routeState(action.payload.url))
},
[NAVIGATE]: (state, action) => {
if (getUrl() !== action.payload.url) {
if (action.payload.silent) {
history.replaceState({}, null, action.payload.url)
} else {
history.pushState({}, null, action.payload.url)
}

return Object.assign({}, state, routeState(action.payload.url))
}
return state
},
})
9 changes: 9 additions & 0 deletions src/router.js
@@ -0,0 +1,9 @@
import uniloc from 'uniloc'

let router = uniloc()

export function configureRouter(routes, aliases) {
router = uniloc(routes, aliases)
}

export default router
6 changes: 6 additions & 0 deletions src/utils.js
@@ -0,0 +1,6 @@
export function getUrl(universal = false, store) {
if (universal) {
return store.getState().router.url
}
return window.location.pathname + window.location.search
}
12 changes: 0 additions & 12 deletions test/test-actions.js

This file was deleted.

48 changes: 22 additions & 26 deletions test/test-init.js
@@ -1,8 +1,14 @@
import init, {getUrl, handlePopState} from '../src/init'
import chai, {expect} from 'chai'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'

const configureSpy = sinon.spy()

const init = proxyquire('../src/init', {
'./router': {configureRouter: configureSpy},
})

chai.use(sinonChai)

describe('init', () => {
Expand All @@ -20,24 +26,8 @@ describe('init', () => {
delete global.window
})

describe('getUrl()', () => {

it('pulls the url from window.location', () => {
const result = getUrl()

expect(result).to.equal('/space/unicorn?lasers=marshmallow')
})

it('pulls the url from the state if this is a universal app', () => {
const store = {
getState: () => ({router: {url: '/space/unicorn?rainbows=delivered'}}),
}

const result = getUrl(true, store)

expect(result).to.equal('/space/unicorn?rainbows=delivered')
})

afterEach(() => {
configureSpy.reset()
})

describe('handlePopState()', () => {
Expand All @@ -50,7 +40,7 @@ describe('init', () => {
type: 'URL_CHANGED',
}

handlePopState(store, {})
init.handlePopState(store)({})

expect(store.dispatch).to.have.been.calledWith(expected)
})
Expand All @@ -77,20 +67,26 @@ describe('init', () => {
delete window.onpopstate
})

it('dispatches an initRouter event', () => {
it('configures the router with the provided routes and aliases', () => {
init.default(store, routes, aliases)

expect(configureSpy).to.have.been.calledWith(routes, aliases)
})

it('dispatches an urlChanged event', () => {
const expected = {
payload: {url: '/space/unicorn?lasers=marshmallow', routes, aliases},
type: 'INIT_ROUTER',
payload: {url: '/space/unicorn?lasers=marshmallow', source: 'init'},
type: 'URL_CHANGED',
}

init(store, routes, aliases)
init.default(store, routes, aliases)

expect(store.dispatch).to.have.been.calledWith(expected)
})

it('attaches an event handler to window.onpopstate', () => {
init(store, routes, aliases)
expect(window).to.have.property('onpopstate').and.equal(handlePopState)
init.default(store, routes, aliases)
expect(window).to.have.property('onpopstate').and.be.a('function')
})

})
Expand Down
111 changes: 109 additions & 2 deletions test/test-reducer.js
@@ -1,7 +1,114 @@
import {expect} from 'chai'
import * as actions from '../src/actions'
import chai, {expect} from 'chai'
import reducer, {routeState} from '../src/reducer'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'

chai.use(sinonChai)

describe('reducer', () => {


before(() => {
global.window = {
location: {
pathname: '/space/unicorn',
search: '?lasers=marshmallow',
},
}
})

after(() => {
delete global.window
})

describe('routeState()', () => {

it('packages the route details up for the state', () => {
const expected = {
route: {
name: undefined,
options: {'bigger-on-the-inside': 'true'},
},
url: '/the/tardis?bigger-on-the-inside=true',
}

const result = routeState('/the/tardis?bigger-on-the-inside=true')

expect(result).to.deep.equal(expected)
})

})

describe('URL_CHANGED', () => {

it('handles url changed events', () => {
const action = actions.urlChanged({
url: '/my/awesome/url?awesome=true',
source: 'test',
})
const expected = routeState('/my/awesome/url?awesome=true')

const result = reducer({}, action)

expect(result).to.deep.equal(expected)
})

})

describe('NAVIGATE', () => {

before(() => {
global.history = {
pushState: sinon.spy(),
replaceState: sinon.spy(),
}
})

afterEach(() => {
global.history.pushState.reset()
global.history.replaceState.reset()
})

after(() => {
delete global.history
})

it('handles navigate events', () => {
const action = actions.navigate({url: '/my/awesome/url?awesome=true'})
const expected = routeState('/my/awesome/url?awesome=true')

const result = reducer({}, action)

expect(result).to.deep.equal(expected)
})

it('creates a new history entry with pushState', () => {
const action = actions.navigate({url: '/into/the/tardis'})

reducer({}, action)

expect(global.history.pushState).to.have.been.calledWith({}, null, '/into/the/tardis')
expect(global.history.replaceState).to.not.have.been.called
})

it('uses replaceState if silent is true', () => {
const action = actions.navigate({url: '/into/the/tardis', silent: true})

reducer({}, action)

expect(global.history.pushState).to.not.have.been.called
expect(global.history.replaceState).to.have.been.calledWith({}, null, '/into/the/tardis')
})

it('doesn\'t create a new history entry if the user is already on that location', () => {
const action = actions.navigate({url: '/space/unicorn?lasers=marshmallow'})

reducer({}, action)

expect(global.history.pushState).to.not.have.been.called
expect(global.history.replaceState).to.not.have.been.called
})

})

})
33 changes: 33 additions & 0 deletions test/test-router.js
@@ -0,0 +1,33 @@
import chai, {expect} from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import proxyquire from 'proxyquire'

chai.use(sinonChai)

const unilocSpy = sinon.spy()

const router = proxyquire('../src/router', {
'uniloc': unilocSpy,
})

describe('router', () => {

afterEach(() => {
unilocSpy.reset()
})

describe('configureRouter()', () => {

it('refreshes the router instance with new routes and aliases', () => {
const routes = []
const aliases = {}

router.configureRouter(routes, aliases)

expect(unilocSpy).to.have.been.calledWith(routes, aliases)
})

})

})
39 changes: 39 additions & 0 deletions test/test-utils.js
@@ -0,0 +1,39 @@
import {getUrl} from '../src/utils'
import {expect} from 'chai'

describe('utils', () => {

describe('getUrl()', () => {

before(() => {
global.window = {
location: {
pathname: '/space/unicorn',
search: '?lasers=marshmallow',
},
}
})

after(() => {
delete global.window
})

it('pulls the url from window.location', () => {
const result = getUrl()

expect(result).to.equal('/space/unicorn?lasers=marshmallow')
})

it('pulls the url from the state if this is a universal app', () => {
const store = {
getState: () => ({router: {url: '/space/unicorn?rainbows=delivered'}}),
}

const result = getUrl(true, store)

expect(result).to.equal('/space/unicorn?rainbows=delivered')
})

})

})

0 comments on commit 594e061

Please sign in to comment.