git.fiddlerwoaroof.com
src/action-router.test.js
30db14b1
 import {applyMiddleware, createStore, compose} from 'redux';
 
 import installBrowserRouter from './action-router';
f7417070
 import addChangeUrlEvent from './change-url-event.js';
 import addMissingHistoryEvents from './history-events.js';
30db14b1
 
 function createLocation(path) {
   return {
     hash: '#hash',
     host: 'example.com',
     hostname: 'example',
     origin: '',
     href: '',
     pathname: path,
     port: 80,
     protocol: 'https:'
   };
 }
 
4b39d368
 function createFakeWindow(path='/path/to/thing') {
30db14b1
   const window = {
4b39d368
     location: createLocation(path),
30db14b1
     history: {
       pushState: jest.fn(),
       replaceState: jest.fn()
     }
   };
 
   const map = {};
 
   window.addEventListener = jest.fn((event, cb) => {
     map[event] = cb;
   });
 
   window.dispatchEvent = jest.fn(ev => {
     const evName = ev.type;
     if(map[evName]) {
       map[evName].handleEvent(ev);
     }
   });
 
   return window;
 }
 
4b39d368
 function setupTest(routesConfig, path='/path/to/thing') {
   const window = createFakeWindow(path);
30db14b1
   const mockPushState = window.history.pushState;
f7417070
   addMissingHistoryEvents(window, window.history);
   addChangeUrlEvent(window, window.history);
30db14b1
 
09761a45
   const {middleware, enhancer, init} = installBrowserRouter(routesConfig, window);
   const reduce = jest.fn();
f7417070
 
09761a45
   const store = createStore(
     reduce,
     compose(
       enhancer,
       applyMiddleware(
         middleware)));
30db14b1
 
09761a45
   function urlChanges() {
     return mockPushState.mock.calls.map(item => item[2]);
   }
 
   function actionsDispatched() {
     return reduce.mock.calls.map(item => item[1]).slice(1);
   }
 
   function fireUrlChange(path) {
     window.dispatchEvent(new CustomEvent('urlchanged', {detail: createLocation(path)}));
   }
30db14b1
 
09761a45
   return {store, reduce, window, urlChanges, actionsDispatched, fireUrlChange, init};
30db14b1
 }
 
 it("router handles exact match in preference to wildcard match", () => {
 
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, id: 1};
   const routesConfig = [
     ["/somewhere/:id", actionType, {}],
     ["/somewhere", actionType, {id: 1}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then
   expect(urlChanges()).toEqual(['/somewhere']);
 
 });
 
 it("router handles wildcard with extra args correctly", () => {
 
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, id: 1, view: "home"};
   const routesConfig = [
     ["/somewhere/:id/:view", actionType, {}],
     ["/somewhere/:id/default", actionType, {view: "home"}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then
   expect(urlChanges()).toEqual(['/somewhere/1/default']);
 
 });
 
 
 it("router handles wildcard with extraArgs correctly with reverse order", () => {
 
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, id: 1, view: "home"};
   const routesConfig = [
     ["/somewhere/:id/default", actionType, {view: "home"}],
     ["/somewhere/:id/:view", actionType, {}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then
   expect(urlChanges()).toEqual(['/somewhere/1/default']);
 
 });
 
 it("router handles wildcard without extraArgs correctly", () => {
 
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, id: 1};
   const routesConfig = [
     ["/somewhere/:id/default", actionType, {}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then
   expect(urlChanges()).toEqual(['/somewhere/1/default']);
 
 });
 
 it("router handles wildcard with no match correctly", () => {
 
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, foo: 1};
   const routesConfig = [
     ["/somewhere/:id/default", actionType, {}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then ( no url changes triggered)
   expect(urlChanges()).toEqual([]);
 
 });
 
 it("router does not match when all args are not accounted for", () => {
   //given
   const actionType = 'THE_ACTION';
   const action = {type: actionType, id: 1, view: "home"};
   const routesConfig = [
     ["/somewhere/:id/default", actionType, {}],
   ];
   const {urlChanges, store} = setupTest(routesConfig);
 
   // when
   store.dispatch(action);
 
   // then ( no url changes triggered)
   expect(urlChanges()).toEqual([]);
 });
 
 it("router should match non-wildcard route in preference to wildcard route", () => {
   // given
   const routesConfig = [
     ['/somewhere/:id', 'ACTION_NAME', {}],
     ["/somewhere/specific", 'ACTION_NAME', {id: 1}],
   ];
   const {actionsDispatched, fireUrlChange} = setupTest(routesConfig);
 
   // when
   fireUrlChange('/somewhere/specific');
 
   // then
   expect(actionsDispatched()).toEqual([{type: 'ACTION_NAME', id: 1}]);
 });
 
10f5ef83
 it("router should throw on duplicate paths", () => {
   // given
   const routesConfig = [
     ['/somewhere/:id', 'ACTION_NAME', {}],
     ["/somewhere/:id", 'ACTION_NAME', {}],
   ];
 
   expect( () => {
     setupTest(routesConfig);
   }).toThrow();
 });
 
 it("router should throw on equally specific routes", () => {
   // given
   const routesConfig = [
     ['/somewhere/:id', 'ACTION_NAME', {}],
     ["/somewhere/:specific", 'ACTION_NAME', {}],
   ];
 
   expect( () => {
65acc8b5
     setupTest(routesConfig);
10f5ef83
   }).toThrow();
 });
 
30db14b1
 it("router should match less-wildcarded routes in preference to more wildcarded routes", () => {
   //given
   const routesConfig = [
10f5ef83
     ["/somewhere/:id/:view/:bar", "ACTION_NAME", {}],
     ["/somewhere/:foo/:id/:view/:baz", "ACTION_NAME", {}],
30db14b1
   ];
   const {actionsDispatched, fireUrlChange} = setupTest(routesConfig);
 
   // when
10f5ef83
   fireUrlChange('/somewhere/specific/etc/bar');
30db14b1
 
   // then
10f5ef83
   expect(actionsDispatched()).toEqual([{type:'ACTION_NAME', id: "specific", view: "etc", bar: "bar"}]);
d93719b8
 
 });
 
 it("router should propagate matches through non-matching cases", () => {
   //given
   const routesConfig = [
     ["/somewhere/specific/:view", "ACTION_NAME", {id: 1}],
     ["/somewhere/:id/:view", "ACTION_NAME", {}],
     ["/not/a/match", "ACTION_NAME", {}]
   ];
   const {actionsDispatched, fireUrlChange} = setupTest(routesConfig);
 
   // when
   fireUrlChange('/somewhere/specific/etc');
 
   // then
30db14b1
   expect(actionsDispatched()).toEqual([{type:'ACTION_NAME', id: 1, view: "etc"}]);
 
 });
 
 it("router should give precedence to exact match first in equally-specific routes (/a/:b vs /:a/b)", () => {
   // given
   const routesConfig = [
     ["/something/:dynamic", "ACTION_NAME", {}],
     ["/:dyn/something", "ACTION_NAME", {}],
   ];
   const {actionsDispatched, fireUrlChange} = setupTest(routesConfig);
 
   // when
   fireUrlChange("/something/something");
 
   // then
   expect(actionsDispatched()).toEqual([{type: 'ACTION_NAME', dynamic: 'something'}]);
 
 });
4b39d368
 
 it("router handles the current location when initialized", () => {
   // given
   const routesConfig = [
     ["/something/:dynamic", "ACTION_NAME", {}],
     ["/:dyn/something", "ACTION_NAME", {}],
   ];
 
   // when
   /// We break the pattern because we're testing store construction.
09761a45
   const {actionsDispatched, init} = setupTest(routesConfig, '/something/something');
   init();
4b39d368
 
   // then
   expect(actionsDispatched()).toEqual([{type: 'ACTION_NAME', dynamic: 'something'}]);
 
 });