Skip to content

Commit

Permalink
HashRouter hashType implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Whaileee committed Mar 20, 2024
1 parent c7dd3d3 commit f1a9dd4
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .changeset/khaki-beers-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-router": patch
"react-router-dom": patch
---

HashRouter hashType implementation for backwards compatibility with project migrating from React-Router v4/v5
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,4 @@
- yracnet
- yuleicul
- zheng-chuang
- Whaileee
17 changes: 17 additions & 0 deletions docs/router-components/hash-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare function HashRouter(

interface HashRouterProps {
basename?: string;
hashType?: HashType
children?: React.ReactNode;
future?: FutureConfig;
window?: Window;
Expand Down Expand Up @@ -55,6 +56,22 @@ function App() {
}
```

## `hashType`

Decide wether to put a slash after the '#' in the URL (default: 'slash')

```jsx
function App() {
return (
<HashRouter hashType='noslash'>
<Routes>
<Route path="/bookmark" /> {/* 👈 Renders at /#bookmark/ */}
</Routes>
</HashRouter>
);
}
```

## `future`

An optional set of [Future Flags][api-development-strategy] to enable. We recommend opting into newly released future flags sooner rather than later to ease your eventual migration to v7.
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "52.4 kB"
"none": "52.52 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "14.8 kB"
Expand All @@ -125,10 +125,10 @@
"none": "17.2 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "17.1 kB"
"none": "17.11 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "23.5 kB"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,41 @@ describe("Handles concurrent mode features during navigations", () => {

await assertNavigation(container, resolve, resolveLazy);
});
// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, LazyComponent, resolve, resolveLazy } =
getComponents();

let { container } = render(
<HashRouter
window={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
hashType="noslash"
>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={
<React.Suspense fallback={<p>Loading...</p>}>
<About />
</React.Suspense>
}
/>
<Route
path="/lazy"
element={
<React.Suspense fallback={<p>Loading Lazy Component...</p>}>
<LazyComponent />
</React.Suspense>
}
/>
</Routes>
</HashRouter>
);

await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("RouterProvider", async () => {
Expand Down Expand Up @@ -289,7 +324,6 @@ describe("Handles concurrent mode features during navigations", () => {
await waitFor(() => screen.getByText("Lazy"));
expect(getHtml(container)).toMatch("Lazy");
}

// eslint-disable-next-line jest/expect-expect
it("MemoryRouter", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
Expand All @@ -308,6 +342,28 @@ describe("Handles concurrent mode features during navigations", () => {
await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
getComponents();

let { container } = render(
<HashRouter
window-={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
hashType="noslash"
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/lazy" element={<LazyComponent />} />
</Routes>
</HashRouter>
);

await assertNavigation(container, resolve, resolveLazy);
});

// eslint-disable-next-line jest/expect-expect
it("BrowserRouter", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);

testDomRouter("<DataHashRouterNoSlash>", (routes, opts) => createHashRouter(routes, { ...opts, hashType: 'noslash' }), (url) =>
getWindowImpl(url, true)
);

function testDomRouter(
name: string,
createTestRouter: typeof createBrowserRouter | typeof createHashRouter,
Expand All @@ -33,8 +37,8 @@ function testDomRouter(
let consoleError: jest.SpyInstance;

beforeEach(() => {
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
});

afterEach(() => {
Expand Down
31 changes: 21 additions & 10 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,20 @@ import {

import getHtml from "../../react-router/__tests__/utils/getHtml";
import { createDeferred } from "../../router/__tests__/utils/utils";
function createNoSlashRouter(routes, opts?) {
return opts === undefined ? createHashRouter(routes, { hashType: 'noslash' }) : createHashRouter(routes, { ...opts, hashType: 'noslash' })
}

testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);

testDomRouter("<DataBrowserRouter>", createBrowserRouter, (url) =>
getWindowImpl(url, false)
);

testDomRouter("<DataHashRouter>", createHashRouter, (url) =>
getWindowImpl(url, true)
);
testDomRouter("<DataHashRouterNoSlash>", createNoSlashRouter, (url) =>
getWindowImpl(url.substring(1), true))

function testDomRouter(
name: string,
Expand All @@ -61,6 +67,9 @@ function testDomRouter(
if (name === "<DataHashRouter>") {
// eslint-disable-next-line jest/no-conditional-expect
expect(testWindow.location.hash).toEqual("#" + pathname + (search || ""));
}
else if (name === "<DataHashRouterNoSlash>") {
expect(testWindow.location.hash).toEqual("#" + pathname.slice(1).concat() + (search || ""));
} else {
// eslint-disable-next-line jest/no-conditional-expect
expect(testWindow.location.pathname).toEqual(pathname);
Expand All @@ -75,8 +84,8 @@ function testDomRouter(
let consoleError: jest.SpyInstance;

beforeEach(() => {
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
});

afterEach(() => {
Expand All @@ -90,7 +99,6 @@ function testDomRouter(
createRoutesFromElements(<Route path="/" element={<h1>Home</h1>} />)
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Expand Down Expand Up @@ -4698,6 +4706,9 @@ function testDomRouter(

// Resolve Comp2 loader and complete navigation
navDfd.resolve("nav data");
// On slower machines test could find updated `/2.*idle/` but `["idle"]` hasn't updated yet. This is purely performance issue.
// Sometimes closing all apps and letting test run caused test to pass after many failures
await waitFor(() => screen.getByText(/\[\]/), { timeout: 500 });
await waitFor(() => screen.getByText(/2.*idle/));
expect(getHtml(container.querySelector("#output")!))
.toMatchInlineSnapshot(`
Expand Down Expand Up @@ -5370,9 +5381,9 @@ function testDomRouter(
let { container } = render(<RouterProvider router={router} />);
expect(container.innerHTML).not.toMatch(/my-key/);
await waitFor(() =>
// React `useId()` results in either `:r2a:` or `:rp:` depending on
// `DataBrowserRouter`/`DataHashRouter`
expect(container.innerHTML).toMatch(/(:r2a:|:rp:),my-key/)
// React `useId()` results in either `:r2a:` or `:rp:` or `:r3r:` depending on
// `DataBrowserRouter`/`DataHashRouter`/`DataHashRouterNoSlash`
expect(container.innerHTML).toMatch(/(:r2a:|:rp:|:r3r:),my-key/)
);
});
});
Expand Down Expand Up @@ -7392,7 +7403,7 @@ function testDomRouter(
ready: Promise.resolve(),
finished: Promise.resolve(),
updateCallbackDone: Promise.resolve(),
skipTransition: () => {},
skipTransition: () => { },
};
});
testWindow.document.startViewTransition = spy;
Expand Down
35 changes: 35 additions & 0 deletions packages/react-router-dom/__tests__/link-href-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,23 @@ describe("<Link> href", () => {
);
});

describe("when using a hash router with noslash", () => {
it("renders proper <a href> for HashRouter", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<HashRouter hashType="noslash">
<Routes>
<Route path="/" element={<Link to="/path?search=value#hash" />} />
</Routes>
</HashRouter>
);
});
expect(renderer.root.findByType("a").props.href).toEqual(
"#path?search=value#hash"
);
});

it("renders proper <a href> for createHashRouter", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
Expand All @@ -906,7 +923,25 @@ describe("<Link> href", () => {
"#/path?search=value#hash"
);
});

it("renders proper <a href> for createHashRouter with noslash", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
let router = createHashRouter([
{
path: "/",
element: <Link to="/path?search=value#hash">Link</Link>,
},
],
{hashType: 'noslash'});
renderer = TestRenderer.create(<RouterProvider router={router} />);
});
expect(renderer.root.findByType("a").props.href).toEqual(
"#path?search=value#hash"
);
});
});
});

test("fails gracefully on invalid `to` values", () => {
let warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe("v7_partialHydration", () => {
describe("createHashRouter", () => {
testPartialHydration(createHashRouter, ReactRouterDom_RouterProvider);
});
describe("createHashRouter with noslash", () => {
testPartialHydration((routes, opts) => createHashRouter(routes, {...opts, hashType:'noslash'}), ReactRouterDom_RouterProvider);
});

describe("createMemoryRouter", () => {
testPartialHydration(createMemoryRouter, ReactRouter_RouterPRovider);
Expand Down

0 comments on commit f1a9dd4

Please sign in to comment.