git.fiddlerwoaroof.com
src/action-router.test.js
84348047
 import { applyMiddleware, createStore, compose } from "redux";
30db14b1
 
84348047
 import installBrowserRouter from "./action-router";
 import addChangeUrlEvent from "./change-url-event.js";
 import addMissingHistoryEvents from "./history-events.js";
30db14b1
 
7b1d07fc
 const console_log = console.log;
 console.log = () => {};
 function with_console(cb) {
   console.log = console_log;
   try {
     cb();
   } catch (e) {
     console.log = () => {};
     throw e;
   }
   console.log = () => {};
 }
 
30db14b1
 function createLocation(path) {
   return {
84348047
     hash: "#hash",
     host: "example.com",
     hostname: "example",
     origin: "",
     href: "",
30db14b1
     pathname: path,
     port: 80,
84348047
     protocol: "https:"
30db14b1
   };
 }
 
84348047
 function createFakeWindow(path = "/path/to/thing") {
   let locations = [createLocation("(root)")];
7b1d07fc
   function pushLocation(window, path) {
     let newLoc = createLocation(path);
     locations.push(newLoc);
     window.location = newLoc;
     return newLoc;
   }
   function popLocation(window) {
     locations.pop();
84348047
     let newLoc = locations[locations.length - 1];
7b1d07fc
     window.location = newLoc;
     return newLoc;
   }
 
30db14b1
   const window = {
     history: {
84348047
       pushState: jest.fn((_, __, path) => {
         window.location = pushLocation(window, path);
       }),
       replaceState: jest.fn()
30db14b1
     }
   };
 
7b1d07fc
   pushLocation(window, path);
30db14b1
   const map = {};
 
   window.addEventListener = jest.fn((event, cb) => {
     map[event] = cb;
   });
 
7b1d07fc
   function prepareEvent(window, evName) {
84348047
     if (evName == "popstate") {
7b1d07fc
       window.location = popLocation(window);
     }
   }
 
30db14b1
   window.dispatchEvent = jest.fn(ev => {
     const evName = ev.type;
84348047
     if (map[evName]) {
7b1d07fc
       prepareEvent(window, evName);
30db14b1
       map[evName].handleEvent(ev);
     }
   });
 
   return window;
 }
 
84348047
 function setupTest(routesConfig, path = "/path/to/thing") {
4b39d368
   const window = createFakeWindow(path);
30db14b1
   const mockPushState = window.history.pushState;
f7417070
   addMissingHistoryEvents(window, window.history);
   addChangeUrlEvent(window, window.history);
30db14b1
 
84348047
   const { enhancer, init } = installBrowserRouter(routesConfig, window);
09761a45
   const reduce = jest.fn();
f7417070
 
84348047
   const store = createStore(reduce, enhancer);
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) {
84348047
     window.dispatchEvent(
       new CustomEvent("urlchanged", { detail: createLocation(path) })
     );
09761a45
   }
30db14b1
 
84348047
   return {
     store,
     reduce,
     window,
     urlChanges,
     actionsDispatched,
     fireUrlChange,
     init
   };
30db14b1
 }
 
 it("router handles exact match in preference to wildcard match", () => {
   //given
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, id: 1 };
30db14b1
   const routesConfig = [
     ["/somewhere/:id", actionType, {}],
84348047
     ["/somewhere", actionType, { id: 1 }]
30db14b1
   ];
84348047
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // when
   store.dispatch(action);
 
   // then
84348047
   expect(urlChanges()).toEqual(["/somewhere"]);
30db14b1
 });
 
7e700dcc
 it("router doees not dispatch an action from url change that is caused by action dispatch", () => {
   //given
84348047
   const actionType = "THE_ACTION";
7e700dcc
   const id = "1";
   const view = "home";
84348047
   const action = { type: actionType, id, view };
7e700dcc
   const routesConfig = [
     ["/somewhere/:id/:view", actionType, {}],
84348047
     ["/somewhere/:id/default", actionType, { view: "home" }]
7e700dcc
   ];
84348047
   const { urlChanges, store, actionsDispatched, init } = setupTest(
     routesConfig
   );
7e700dcc
 
   // when
   store.dispatch(action);
 
   // then
   expect(actionsDispatched()).toEqual([action]);
 });
 
7b1d07fc
 it("popstate doesn't cause a pushstate", () => {
   //given
84348047
   const actionType = "THE_ACTION";
7b1d07fc
   const id = "1";
   const view = "home";
84348047
   const action = { type: actionType, id, view };
7b1d07fc
   const routesConfig = [
     ["/somewhere/:id/:view", actionType, {}],
84348047
     ["/somewhere/:id/default", actionType, { view: "home" }]
7b1d07fc
   ];
 
   const {
     urlChanges,
     store,
     actionsDispatched,
     fireUrlChange,
     init,
     window
84348047
   } = setupTest(routesConfig, "/somewhere/foo/default");
7b1d07fc
 
   init();
84348047
   window.history.pushState({}, "", "/somwhere/bar/default");
7b1d07fc
 
   // when
84348047
   window.dispatchEvent(new CustomEvent("popstate", {}));
7b1d07fc
 
   // then
   expect(urlChanges().length).toEqual(1);
 });
 
30db14b1
 it("router handles wildcard with extra args correctly", () => {
   //given
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, id: 1, view: "home" };
30db14b1
   const routesConfig = [
     ["/somewhere/:id/:view", actionType, {}],
84348047
     ["/somewhere/:id/default", actionType, { view: "home" }]
30db14b1
   ];
84348047
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // when
   store.dispatch(action);
 
   // then
84348047
   expect(urlChanges()).toEqual(["/somewhere/1/default"]);
30db14b1
 });
 
 it("router handles wildcard with extraArgs correctly with reverse order", () => {
   //given
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, id: 1, view: "home" };
30db14b1
   const routesConfig = [
84348047
     ["/somewhere/:id/default", actionType, { view: "home" }],
     ["/somewhere/:id/:view", actionType, {}]
30db14b1
   ];
84348047
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // when
   store.dispatch(action);
 
   // then
84348047
   expect(urlChanges()).toEqual(["/somewhere/1/default"]);
30db14b1
 });
 
 it("router handles wildcard without extraArgs correctly", () => {
   //given
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, id: 1 };
   const routesConfig = [["/somewhere/:id/default", actionType, {}]];
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // when
   store.dispatch(action);
 
   // then
84348047
   expect(urlChanges()).toEqual(["/somewhere/1/default"]);
30db14b1
 });
 
 it("router handles wildcard with no match correctly", () => {
   //given
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, foo: 1 };
   const routesConfig = [["/somewhere/:id/default", actionType, {}]];
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // 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
84348047
   const actionType = "THE_ACTION";
   const action = { type: actionType, id: 1, view: "home" };
   const routesConfig = [["/somewhere/:id/default", actionType, {}]];
   const { urlChanges, store } = setupTest(routesConfig);
30db14b1
 
   // 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 = [
84348047
     ["/somewhere/:id", "ACTION_NAME", {}],
     ["/somewhere/specific", "ACTION_NAME", { id: 1 }]
30db14b1
   ];
84348047
   const { actionsDispatched, fireUrlChange } = setupTest(routesConfig);
30db14b1
 
   // when
84348047
   fireUrlChange("/somewhere/specific");
30db14b1
 
   // then
84348047
   expect(actionsDispatched()).toEqual([{ type: "ACTION_NAME", id: 1 }]);
30db14b1
 });
 
10f5ef83
 it("router should throw on duplicate paths", () => {
   // given
   const routesConfig = [
84348047
     ["/somewhere/:id", "ACTION_NAME", {}],
     ["/somewhere/:id", "ACTION_NAME", {}]
10f5ef83
   ];
 
84348047
   expect(() => {
10f5ef83
     setupTest(routesConfig);
   }).toThrow();
 });
 
 it("router should throw on equally specific routes", () => {
   // given
   const routesConfig = [
84348047
     ["/somewhere/:id", "ACTION_NAME", {}],
     ["/somewhere/:specific", "ACTION_NAME", {}]
10f5ef83
   ];
 
84348047
   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", {}],
84348047
     ["/somewhere/:foo/:id/:view/:baz", "ACTION_NAME", {}]
30db14b1
   ];
84348047
   const { actionsDispatched, fireUrlChange } = setupTest(routesConfig);
30db14b1
 
   // when
84348047
   fireUrlChange("/somewhere/specific/etc/bar");
30db14b1
 
   // then
84348047
   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 = [
84348047
     ["/somewhere/specific/:view", "ACTION_NAME", { id: 1 }],
d93719b8
     ["/somewhere/:id/:view", "ACTION_NAME", {}],
     ["/not/a/match", "ACTION_NAME", {}]
   ];
84348047
   const { actionsDispatched, fireUrlChange } = setupTest(routesConfig);
d93719b8
 
   // when
84348047
   fireUrlChange("/somewhere/specific/etc");
d93719b8
 
   // then
84348047
   expect(actionsDispatched()).toEqual([
     { type: "ACTION_NAME", id: 1, view: "etc" }
   ]);
30db14b1
 });
 
 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", {}],
84348047
     ["/:dyn/something", "ACTION_NAME", {}]
30db14b1
   ];
84348047
   const { actionsDispatched, fireUrlChange } = setupTest(routesConfig);
30db14b1
 
   // when
   fireUrlChange("/something/something");
 
   // then
84348047
   expect(actionsDispatched()).toEqual([
     { type: "ACTION_NAME", dynamic: "something" }
   ]);
30db14b1
 });
4b39d368
 
 it("router handles the current location when initialized", () => {
   // given
   const routesConfig = [
     ["/something/:dynamic", "ACTION_NAME", {}],
84348047
     ["/:dyn/something", "ACTION_NAME", {}]
4b39d368
   ];
 
   // when
   /// We break the pattern because we're testing store construction.
84348047
   const { actionsDispatched, init } = setupTest(
     routesConfig,
     "/something/something"
   );
09761a45
   init();
4b39d368
 
   // then
84348047
   expect(actionsDispatched()).toEqual([
     { type: "ACTION_NAME", dynamic: "something" }
   ]);
4b39d368
 });
478c0afb
 
 it("pathForAction should render a route", () => {
   // given
   const routesConfig = [
     ["/something/:dynamic", "ACTION_NAME", {}],
84348047
     ["/:dyn/something", "ACTION_NAME", {}]
478c0afb
   ];
84348047
   const action = { type: "ACTION_NAME", dynamic: "hooray" };
   const { store } = setupTest(routesConfig);
478c0afb
   // when
   const actual = store.pathForAction(action);
 
   // then
84348047
   expect(actual).toEqual("/something/hooray");
478c0afb
 });
7b1d07fc
 
 console.log = console_log;