Repository: infernojs/inferno (js)

Issue:
Passing state with a Router link no longer works with inferno-router `8.0.3`
I also couldn't find any unit tests for state.

Example:

```tsx
<NavLink
  to={{
    pathname: "/create_post",
    search: "",
    key: "",
    hash: "",
    state: {some: "state here"}
  }}
>a link
</NavLink>
```

State ends up as `null`



Pull request:
Fix inferno router #1608
Closes #1608

Patch:
diff --git a/packages/inferno-router/src/Link.ts b/packages/inferno-router/src/Link.ts
index fe6a51e31..b1bc5e248 100644
--- a/packages/inferno-router/src/Link.ts
+++ b/packages/inferno-router/src/Link.ts
@@ -4,6 +4,8 @@ import { invariant } from './utils';
 import { combineFrom, isString } from 'inferno-shared';
 import type { Location } from 'history';
 import { parsePath } from 'history';
+import { splitLocation, normalizeToLocation } from "./locationUtils";
+
 
 const isModifiedEvent = (event: InfernoMouseEvent<any>): boolean => Boolean(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
 
@@ -31,12 +33,14 @@ function handleClick({ props, context }, event: InfernoMouseEvent<any>) {
     event.preventDefault();
 
     const { history } = context.router;
-    const { replace = false, to } = props;
+    const { replace = false, to: toPropIn } = props;
+    const { to, state } = splitLocation(normalizeToLocation(toPropIn));
+
 
     if (replace) {
-      history.replace(to);
+      history.replace(to, state);
     } else {
-      history.push(to);
+      history.push(to, state);
     }
   }
 }
diff --git a/packages/inferno-router/src/locationUtils.ts b/packages/inferno-router/src/locationUtils.ts
new file mode 100644
index 000000000..86985ea45
--- /dev/null
+++ b/packages/inferno-router/src/locationUtils.ts
@@ -0,0 +1,11 @@
+import { To, Location, parsePath } from "history";
+import { isString } from 'inferno-shared';
+
+export const normalizeToLocation = (to) => {
+  return isString(to) ? parsePath(to) : to;
+};
+
+export const splitLocation = (location: Location): { to: To, state: any} => {
+    const { key = '', state, ...to } = location;
+    return { to, state };
+}
\ No newline at end of file


Test patch:
diff --git a/packages/inferno-router/__tests__/Link.spec.jsx b/packages/inferno-router/__tests__/Link.spec.jsx
index f230ecdbc..c0aef1930 100644
--- a/packages/inferno-router/__tests__/Link.spec.jsx
+++ b/packages/inferno-router/__tests__/Link.spec.jsx
@@ -1,6 +1,6 @@
-import { render } from 'inferno';
+import { render, Component } from 'inferno';
 import { HashRouter, Link, MemoryRouter } from 'inferno-router';
-import { parsePath } from 'history';
+import { parsePath, createMemoryHistory } from 'history';
 
 describe('Link (jsx)', () => {
   let node;
@@ -85,4 +85,88 @@ describe('A <Link> underneath a <HashRouter>', () => {
     const linkNode = createLinkNode('foo');
     expect(linkNode.getAttribute('href')).toEqual('#foo');
   });
+
+  it('accepts a string `to` prop', () => {
+    const to = '/the/path?the=query#the-hash';
+
+    render(
+      <MemoryRouter>
+        <Link to={to}>link</Link>
+      </MemoryRouter>,
+      node
+    );
+
+    const a = node.querySelector('a');
+
+    expect(a.getAttribute('href')).toEqual('/the/path?the=query#the-hash');
+  });
+
+  it('accepts an object `to` prop', () => {
+    const to = {
+      pathname: '/the/path',
+      search: 'the=query',
+      hash: '#the-hash'
+    };
+
+    render(
+      <MemoryRouter>
+        <Link to={to}>link</Link>
+      </MemoryRouter>,
+      node
+    );
+
+    const a = node.querySelector('a');
+
+    expect(a.getAttribute('href')).toEqual('/the/path?the=query#the-hash');
+  });
+
+  it('accepts an object `to` prop with state', () => {
+    const memoryHistoryFoo = createMemoryHistory({
+      initialEntries: ['/foo']
+    });
+    memoryHistoryFoo.push = jest.fn();
+
+    const clickHandler = jest.fn();
+
+    const to = {
+      pathname: '/the/path',
+      search: 'the=query',
+      hash: '#the-hash',
+      state: { test: 'ok' }
+    };
+
+    class ContextChecker extends Component {
+      getChildContext() {
+        const { context } = this;
+        context.router.history = memoryHistoryFoo;
+
+        return {
+          router: context.router
+        };
+      }
+
+      render({ children }) {
+        return children;
+      }
+    }
+
+    render(
+      <MemoryRouter>
+        <ContextChecker>
+          <Link to={to} onClick={clickHandler}>
+            link
+          </Link>
+        </ContextChecker>
+      </MemoryRouter>,
+      node
+    );
+
+    const a = node.querySelector('a');
+    a.click();
+
+    expect(clickHandler).toBeCalledTimes(1);
+    expect(memoryHistoryFoo.push).toBeCalledTimes(1);
+    const { hash, pathname, search, state } = to;
+    expect(memoryHistoryFoo.push).toBeCalledWith({ hash, pathname, search }, state);
+  });
 });
diff --git a/packages/inferno-router/__tests__/NavLink.spec.tsx b/packages/inferno-router/__tests__/NavLink.spec.tsx
index c27defc33..ba9e9ed80 100644
--- a/packages/inferno-router/__tests__/NavLink.spec.tsx
+++ b/packages/inferno-router/__tests__/NavLink.spec.tsx
@@ -1,5 +1,6 @@
-import { render } from 'inferno';
-import { MemoryRouter, NavLink } from 'inferno-router';
+import { createMemoryHistory } from 'history';
+import { Component, render } from 'inferno';
+import { HashRouter, MemoryRouter, NavLink } from 'inferno-router';
 
 describe('NavLink', () => {
   let node;
@@ -404,4 +405,125 @@ describe('NavLink', () => {
       expect(a.title).toEqual('pasta');
     });
   });
+
+  describe('A <NavLink> underneath a <HashRouter>', () => {
+    let tmpNode;
+    beforeEach(function () {
+      tmpNode = document.createElement('div');
+      document.body.appendChild(tmpNode);
+    });
+  
+    afterEach(function () {
+      render(null, tmpNode);
+      document.body.removeChild(tmpNode);
+    });
+  
+    const createLinkNode = (to) => {
+      render(
+        <HashRouter>
+          <NavLink to={to} />
+        </HashRouter>,
+        tmpNode
+      );
+  
+      return tmpNode.querySelector('a');
+    };
+  
+    it('has the correct href', () => {
+      const linkNode = createLinkNode('/foo');
+      expect(linkNode.getAttribute('href')).toEqual('#/foo');
+    });
+  
+    it('has the correct href #2', () => {
+      const linkNode = createLinkNode('foo');
+      expect(linkNode.getAttribute('href')).toEqual('#foo');
+    });
+  
+    it('accepts a string `to` prop', () => {
+      const to = '/the/path?the=query#the-hash';
+  
+      render(
+        <MemoryRouter>
+          <NavLink to={to}>link</NavLink>
+        </MemoryRouter>,
+        tmpNode
+      );
+  
+      const a = tmpNode.querySelector('a');
+  
+      expect(a.getAttribute('href')).toEqual('/the/path?the=query#the-hash');
+    });
+  
+    it('accepts an object `to` prop', () => {
+      const to = {
+        hash: '#the-hash',
+        key: '1',
+        pathname: '/the/path',
+        search: 'the=query',
+        state: null,
+      };
+  
+      render(
+        <MemoryRouter>
+          <NavLink to={to}>link</NavLink>
+        </MemoryRouter>,
+        tmpNode
+      );
+  
+      const a = tmpNode.querySelector('a');
+  
+      expect(a.getAttribute('href')).toEqual('/the/path?the=query#the-hash');
+    });
+  
+    it('accepts an object `to` prop with state', async () => {
+      const memoryHistoryFoo = createMemoryHistory({
+        initialEntries: ["/foo"]
+      });
+      memoryHistoryFoo.push = jest.fn();
+  
+      const clickHandler = jest.fn();
+      
+      const to = {
+        hash: '#the-hash',
+        key: '1',
+        pathname: '/the/path',
+        search: 'the=query',
+        state: { test: 'ok' }
+      };
+  
+      class ContextChecker extends Component {
+        public getChildContext() {
+          const { context } = this;
+          context.router.history = memoryHistoryFoo;
+  
+          return {
+            router: context.router
+          };
+        }
+      
+        public render({ children }) {
+          return children;
+        }
+      }    
+  
+      render(
+        <MemoryRouter>
+          <ContextChecker>
+            <NavLink to={to} onClick={clickHandler}>
+              link
+            </NavLink>
+          </ContextChecker>
+        </MemoryRouter>,
+        tmpNode
+      );
+  
+      const a = tmpNode.querySelector("a");
+      a.click();
+  
+      expect(clickHandler).toBeCalledTimes(1);
+      expect(memoryHistoryFoo.push).toBeCalledTimes(1);
+      const { hash, pathname, search, state } = to;
+      expect(memoryHistoryFoo.push).toBeCalledWith({ hash, pathname, search }, state);
+    });
+  });
 });