├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── MemoryRouterProvider ├── next-10 │ └── package.json ├── next-11 │ └── package.json ├── next-12 │ └── package.json ├── next-13.5 │ └── package.json ├── next-13 │ └── package.json └── package.json ├── README.md ├── async └── package.json ├── dynamic-routes ├── next-10 │ └── package.json ├── next-11 │ └── package.json ├── next-12 │ └── package.json ├── next-13 │ └── package.json └── package.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── MemoryRouter.test.tsx ├── MemoryRouter.tsx ├── MemoryRouterContext.tsx ├── MemoryRouterProvider │ ├── MemoryRouterProvider.async.test.tsx │ ├── MemoryRouterProvider.test.tsx │ ├── MemoryRouterProvider.tsx │ ├── index.ts │ ├── next-10 │ │ └── index.tsx │ ├── next-11 │ │ └── index.tsx │ ├── next-12 │ │ └── index.tsx │ ├── next-13.5 │ │ └── index.tsx │ └── next-13 │ │ └── index.tsx ├── async │ ├── index.test.tsx │ └── index.tsx ├── dynamic-routes │ ├── createDynamicRouteParser.test.tsx │ ├── createDynamicRouteParser.tsx │ ├── index.ts │ ├── next-10 │ │ └── index.ts │ ├── next-11 │ │ └── index.ts │ ├── next-12 │ │ └── index.ts │ └── next-13 │ │ └── index.ts ├── index.test.tsx ├── index.tsx ├── lib │ └── mitt │ │ └── index.ts ├── navigation │ ├── index.test.tsx │ └── index.tsx ├── next-link.test.tsx ├── urls.ts ├── useMemoryRouter.test.tsx ├── useMemoryRouter.tsx ├── withMemoryRouter.test.tsx └── withMemoryRouter.tsx ├── test ├── example-app │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components │ │ ├── Search.stories.tsx │ │ ├── Search.test.tsx │ │ └── Search.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ └── as-tests │ │ │ ├── [[...route]].tsx │ │ │ ├── as-path.tsx │ │ │ └── real-path.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── next-10 │ ├── jest.config.js │ ├── next-10.test.ts │ ├── package-lock.json │ └── package.json ├── next-11 │ ├── jest.config.js │ ├── next-11.test.ts │ ├── package-lock.json │ └── package.json ├── next-12.2+ │ ├── jest.config.js │ ├── next-12.2.test.ts │ ├── package-lock.json │ └── package.json ├── next-12.latest │ ├── .npmrc │ ├── jest.config.js │ ├── next-12.latest.test.ts │ └── package.json ├── next-12 │ ├── jest.config.js │ ├── next-12.test.ts │ ├── package-lock.json │ └── package.json ├── next-13.5 │ ├── jest.config.js │ ├── next-13.5.test.ts │ ├── package-lock.json │ └── package.json ├── next-13 │ ├── jest.config.js │ ├── next-13.test.ts │ ├── package-lock.json │ └── package.json ├── test-exports │ ├── exports.test.ts │ ├── jest.config.js │ └── package.json └── test-utils.ts ├── tsconfig.build.json └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | max_line_length = 120 3 | indent_size = 2 4 | indent_style = space 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**' 9 | - '!README.md' 10 | pull_request: 11 | paths: 12 | - '**' 13 | - '!README.md' 14 | 15 | jobs: 16 | test: 17 | name: "${{ matrix.command }}" 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | command: 22 | - npm run build 23 | - npm run lint 24 | - npm run test 25 | - npm run test:integration 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | cache: "npm" 32 | cache-dependency-path: "**/package-lock.json" 33 | 34 | - name: Installation 35 | run: npm ci 36 | 37 | - name: Installation (for Integration Tests) 38 | if: contains(matrix.command, 'integration') 39 | run: npm run test:integration:install 40 | 41 | - name: "${{ matrix.command }}" 42 | run: ${{ matrix.command }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - main-test-changesets 8 | 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18.x 20 | cache: "npm" 21 | cache-dependency-path: "**/package-lock.json" 22 | 23 | - name: Installation 24 | run: npm ci 25 | 26 | - name: Create Release Pull Request or Publish to npm 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 31 | publish: npm run release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE generated 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # asdf files 110 | .tool-versions 111 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottrippey/next-router-mock/aa891b21af1168e32bb6eb5a4279e86ccf1dcca9/.prettierrc -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # next-router-mock 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 4a75df0: Removed debugger statement. 8 | 9 | ## 1.0.1 10 | 11 | ### Patch Changes 12 | 13 | - c1c9da4: chore: update package.json exports to include TypeScript types 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 58b058c: # Next 13 App Router Support 20 | - Adds support for mocking `next/navigation` 21 | - Adds `mockRouter.reset()` for easy resetting before tests 22 | 23 | ## 0.9.13 24 | 25 | ### Patch Changes 26 | 27 | - 5afd47b: Add support for parsing `search` parameter for URLs 28 | 29 | ## 0.9.12 30 | 31 | ### Patch Changes 32 | 33 | - d2e8fff: Enable MemoryRouterProvider to use the async singleton 34 | 35 | ## 0.9.11 36 | 37 | ### Patch Changes 38 | 39 | - 4603169: fix: update UrlObject type to match native url module 40 | 41 | ## 0.9.10 42 | 43 | ### Patch Changes 44 | 45 | - c8270b3: Updated `MemoryRouterProvider` to support Next 13.5 46 | 47 | ## 0.9.9 48 | 49 | ### Patch Changes 50 | 51 | - 41e335c: - Better support for the "as" parameter 52 | - Implemented correct behavior when "as" path is different than the real path 53 | - Fixes #89 54 | 55 | ## 0.9.8 56 | 57 | ### Patch Changes 58 | 59 | - 105a8bc: Added automated releases and a changelog! 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Scott Rippey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MemoryRouterProvider/next-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/MemoryRouterProvider/next-10" 3 | } 4 | -------------------------------------------------------------------------------- /MemoryRouterProvider/next-11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/MemoryRouterProvider/next-11" 3 | } 4 | -------------------------------------------------------------------------------- /MemoryRouterProvider/next-12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/MemoryRouterProvider/next-12" 3 | } 4 | -------------------------------------------------------------------------------- /MemoryRouterProvider/next-13.5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/MemoryRouterProvider/next-13.5" 3 | } 4 | -------------------------------------------------------------------------------- /MemoryRouterProvider/next-13/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/MemoryRouterProvider/next-13" 3 | } 4 | -------------------------------------------------------------------------------- /MemoryRouterProvider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/MemoryRouterProvider/index" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `next-router-mock` 2 | 3 | An implementation of the Next.js Router that keeps the state of the "URL" in memory (does not read or write to the 4 | address bar). Useful in **tests** and **Storybook**. 5 | Inspired by [`react-router > MemoryRouter`](https://github.com/remix-run/react-router/blob/main/docs/router-components/memory-router.md). 6 | 7 | Tested with NextJS v13, v12, v11, and v10. 8 | 9 | Install via NPM: `npm install --save-dev next-router-mock` 10 | 11 | For usage with `next/navigation` jump to [Usage with next/navigation Beta](#usage-with-nextnavigation-beta) 12 | 13 | 14 | 15 | 16 | - [Usage with Jest](#usage-with-jest) 17 | - [Jest Configuration](#jest-configuration) 18 | - [Jest Example](#jest-example) 19 | - [Usage with Storybook](#usage-with-storybook) 20 | - [Storybook Configuration](#storybook-configuration) 21 | - [Storybook Example](#storybook-example) 22 | - [Compatibility with `next/link`](#compatibility-with-nextlink) 23 | - [Example: `next/link` with React Testing Library](#example-nextlink-with-react-testing-library) 24 | - [Example: `next/link` with Enzyme](#example-nextlink-with-enzyme) 25 | - [Example: `next/link` with Storybook](#example-nextlink-with-storybook) 26 | - [Dynamic Routes](#dynamic-routes) 27 | - [Sync vs Async](#sync-vs-async) 28 | - [Supported Features](#supported-features) 29 | - [Not yet supported](#not-yet-supported) 30 | - [Usage with next/navigation Beta](#usage-with-nextnavigation-beta) 31 | - [Usage with Jest](#usage-with-jest-1) 32 | - [Jest Configuration](#jest-configuration-1) 33 | - [Jest Example](#jest-example-1) 34 | - [Supported](#supported) 35 | - [Not supported yet](#not-supported-yet) 36 | 37 | 38 | 39 | # Usage with Jest 40 | 41 | ### Jest Configuration 42 | 43 | For unit tests, the `next-router-mock` module can be used as a drop-in replacement for `next/router`: 44 | 45 | ```js 46 | jest.mock("next/router", () => require("next-router-mock")); 47 | ``` 48 | 49 | You can do this once per spec file, or you can [do this globally using `setupFilesAfterEnv`](https://jestjs.io/docs/configuration/#setupfilesafterenv-array). 50 | 51 | ### Jest Example 52 | 53 | In your tests, use the router from `next-router-mock` to set the current URL and to make assertions. 54 | 55 | ```jsx 56 | import { useRouter } from "next/router"; 57 | import { render, screen, fireEvent } from "@testing-library/react"; 58 | import mockRouter from "next-router-mock"; 59 | 60 | jest.mock("next/router", () => jest.requireActual("next-router-mock")); 61 | 62 | const ExampleComponent = ({ href = "" }) => { 63 | const router = useRouter(); 64 | return ; 65 | }; 66 | 67 | describe("next-router-mock", () => { 68 | it("mocks the useRouter hook", () => { 69 | // Set the initial url: 70 | mockRouter.push("/initial-path"); 71 | 72 | // Render the component: 73 | render(); 74 | expect(screen.getByRole("button")).toHaveTextContent('The current route is: "/initial-path"'); 75 | 76 | // Click the button: 77 | fireEvent.click(screen.getByRole("button")); 78 | 79 | // Ensure the router was updated: 80 | expect(mockRouter).toMatchObject({ 81 | asPath: "/foo?bar=baz", 82 | pathname: "/foo", 83 | query: { bar: "baz" }, 84 | }); 85 | }); 86 | }); 87 | ``` 88 | 89 | # Usage with Storybook 90 | 91 | ### Storybook Configuration 92 | 93 | Globally enable `next-router-mock` by adding the following webpack alias to your Storybook configuration. 94 | 95 | In `.storybook/main.js` add: 96 | 97 | ```js 98 | module.exports = { 99 | webpackFinal: async (config, { configType }) => { 100 | config.resolve.alias = { 101 | ...config.resolve.alias, 102 | "next/router": "next-router-mock", 103 | }; 104 | return config; 105 | }, 106 | }; 107 | ``` 108 | 109 | This ensures that all your components that use `useRouter` will work in Storybook. If you also need to test `next/link`, please see the section [Example: **`next/link` with Storybook**](#example-nextlink-with-storybook). 110 | 111 | ### Storybook Example 112 | 113 | In your individual stories, you might want to mock the current URL (eg. for testing an "ActiveLink" component), or you might want to log `push/replace` actions. You can do this by wrapping your stories with the `` component. 114 | 115 | ```jsx 116 | // ActiveLink.story.jsx 117 | import { action } from "@storybook/addon-actions"; 118 | import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider/next-13"; 119 | import { ActiveLink } from "./active-link"; 120 | 121 | export const ExampleStory = () => ( 122 | 123 | Not Active 124 | Active 125 | 126 | ); 127 | ``` 128 | 129 | > Be sure to import from **a matching Next.js version**: 130 | > 131 | > ``` 132 | > import { MemoryRouterProvider } 133 | > from 'next-router-mock/MemoryRouterProvider/next-13.5'; 134 | > ``` 135 | > 136 | > Choose from `next-13.5`, `next-13`, `next-12`, or `next-11`. 137 | 138 | The `MemoryRouterProvider` has the following optional properties: 139 | 140 | - `url` (`string` or `object`) sets the current route's URL 141 | - `async` enables async mode, if necessary (see "Sync vs Async" for details) 142 | - Events: 143 | - `onPush(url, { shallow })` 144 | - `onReplace(url, { shallow })` 145 | - `onRouteChangeStart(url, { shallow })` 146 | - `onRouteChangeComplete(url, { shallow })` 147 | 148 | # Compatibility with `next/link` 149 | 150 | To use `next-router-mock` with `next/link`, you must use a `` to wrap the test component. 151 | 152 | ### Example: `next/link` with React Testing Library 153 | 154 | When rendering, simply supply the option `{ wrapper: MemoryRouterProvider }` 155 | 156 | ```jsx 157 | import { render } from "@testing-library/react"; 158 | import NextLink from "next/link"; 159 | 160 | import mockRouter from "next-router-mock"; 161 | import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 162 | 163 | it("NextLink can be rendered", () => { 164 | render(Example Link, { wrapper: MemoryRouterProvider }); 165 | fireEvent.click(screen.getByText("Example Link")); 166 | expect(mockRouter.asPath).toEqual("/example"); 167 | }); 168 | ``` 169 | 170 | ### Example: `next/link` with Enzyme 171 | 172 | When rendering, simply supply the option `{ wrapperComponent: MemoryRouterProvider }` 173 | 174 | ```jsx 175 | import { shallow } from "enzyme"; 176 | import NextLink from "next/link"; 177 | 178 | import mockRouter from "next-router-mock"; 179 | import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 180 | 181 | it("NextLink can be rendered", () => { 182 | const wrapper = shallow(Example Link, { 183 | wrapperComponent: MemoryRouterProvider, 184 | }); 185 | 186 | wrapper.find("a").simulate("click"); 187 | 188 | expect(mockRouter.asPath).to.equal("/example"); 189 | }); 190 | ``` 191 | 192 | ### Example: `next/link` with Storybook 193 | 194 | In Storybook, you must wrap your component with the `` component (with optional `url` set). 195 | 196 | ```jsx 197 | // example.story.jsx 198 | import NextLink from "next/link"; 199 | import { action } from "@storybook/addon-actions"; 200 | 201 | import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider/next-13.5"; 202 | 203 | export const ExampleStory = () => ( 204 | 205 | Example Link 206 | 207 | ); 208 | ``` 209 | 210 | This can be done inline (as above). 211 | It can also be implemented as a `decorator`, which can be per-Story, per-Component, or Global (see [Storybook Decorators Documentation](https://storybook.js.org/docs/react/writing-stories/decorators) for details). 212 | Global example: 213 | 214 | ``` 215 | // .storybook/preview.js 216 | import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider'; 217 | 218 | export const decorators = [ 219 | (Story) => 220 | ]; 221 | ``` 222 | 223 | # Dynamic Routes 224 | 225 | By default, `next-router-mock` does not know about your dynamic routes (eg. files like `/pages/[id].js`). 226 | To test code that uses dynamic routes, you must add the routes manually, like so: 227 | 228 | ```typescript 229 | import mockRouter from "next-router-mock"; 230 | import { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 231 | 232 | mockRouter.useParser( 233 | createDynamicRouteParser([ 234 | // These paths should match those found in the `/pages` folder: 235 | "/[id]", 236 | "/static/path", 237 | "/[dynamic]/path", 238 | "/[...catchAll]/path", 239 | ]) 240 | ); 241 | 242 | // Example test: 243 | it("should parse dynamic routes", () => { 244 | mockRouter.push("/FOO"); 245 | expect(mockRouter).toMatchObject({ 246 | pathname: "/[id]", 247 | query: { id: "FOO" }, 248 | }); 249 | }); 250 | ``` 251 | 252 | # Sync vs Async 253 | 254 | By default, `next-router-mock` handles route changes synchronously. This is convenient for testing, and works for most 255 | use-cases. 256 | However, Next normally handles route changes asynchronously, and in certain cases you might actually rely on that 257 | behavior. If that's the case, you can use `next-router-mock/async`. Tests will need to account for the async behavior 258 | too; for example: 259 | 260 | ```jsx 261 | it("next/link can be tested too", async () => { 262 | render( 263 | 264 | Example Link 265 | 266 | ); 267 | fireEvent.click(screen.getByText("Example Link")); 268 | await waitFor(() => { 269 | expect(singletonRouter).toMatchObject({ 270 | asPath: "/example?foo=bar", 271 | pathname: "/example", 272 | query: { foo: "bar" }, 273 | }); 274 | }); 275 | }); 276 | ``` 277 | 278 | # Supported Features 279 | 280 | - `useRouter()` 281 | - `withRouter(Component)` 282 | - `router.push(url, as?, options?)` 283 | - `router.replace(url, as?, options?)` 284 | - `router.route` 285 | - `router.pathname` 286 | - `router.asPath` 287 | - `router.query` 288 | - Works with `next/link` (see Jest notes) 289 | - `router.events` supports: 290 | - `routeChangeStart(url, { shallow })` 291 | - `routeChangeComplete(url, { shallow })` 292 | - `hashChangeStart(url, { shallow })` 293 | - `hashChangeComplete(url, { shallow })` 294 | 295 | ## Not yet supported 296 | 297 | PRs welcome! 298 | These fields just have default values; these methods do nothing. 299 | 300 | - `router.isReady` 301 | - `router.basePath` 302 | - `router.isFallback` 303 | - `router.isLocaleDomain` 304 | - `router.locale` 305 | - `router.locales` 306 | - `router.defaultLocale` 307 | - `router.domainLocales` 308 | - `router.prefetch()` 309 | - `router.back()` 310 | - `router.beforePopState(cb)` 311 | - `router.reload()` 312 | - `router.events` not implemented: 313 | - `routeChangeError` 314 | - `beforeHistoryChange` 315 | 316 | # Usage with next/navigation Beta 317 | 318 | ## Usage with Jest 319 | 320 | ### Jest Configuration 321 | 322 | For unit tests, the `next-router-mock/navigation` module can be used as a drop-in replacement for `next/navigation`: 323 | 324 | ```js 325 | jest.mock("next/navigation", () => require("next-router-mock/navigation")); 326 | ``` 327 | 328 | You can do this once per spec file, or you can [do this globally using `setupFilesAfterEnv`](https://jestjs.io/docs/configuration/#setupfilesafterenv-array). 329 | 330 | ### Jest Example 331 | 332 | In your tests, use the router from `next-router-mock` to set the current URL and to make assertions. 333 | 334 | ```jsx 335 | import mockRouter from "next-router-mock"; 336 | import { render, screen, fireEvent } from "@testing-library/react"; 337 | import { usePathname, useRouter } from "next/navigation"; 338 | 339 | jest.mock("next/navigation", () => jest.requireActual("next-router-mock/navigation")); 340 | 341 | const ExampleComponent = ({ href = "" }) => { 342 | const router = useRouter(); 343 | const pathname = usePathname(); 344 | return ; 345 | }; 346 | 347 | describe("next-router-mock", () => { 348 | it("mocks the useRouter hook", () => { 349 | // Set the initial url: 350 | mockRouter.push("/initial-path"); 351 | 352 | // Render the component: 353 | render(); 354 | expect(screen.getByRole("button")).toHaveTextContent("The current route is: /initial-path"); 355 | 356 | // Click the button: 357 | fireEvent.click(screen.getByRole("button")); 358 | 359 | // Ensure the router was updated: 360 | expect(mockRouter).toMatchObject({ 361 | asPath: "/foo?bar=baz", 362 | pathname: "/foo", 363 | query: { bar: "baz" }, 364 | }); 365 | }); 366 | }); 367 | ``` 368 | 369 | ### Supported 370 | 371 | - useRouter 372 | - usePathname 373 | - useParams 374 | - useSearchParams 375 | 376 | ### Not supported yet 377 | 378 | - Storybook 379 | - useSelectedLayoutSegment, useSelectedLayoutSegments 380 | - non-hook utils in next/navigation 381 | -------------------------------------------------------------------------------- /async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/async" 3 | } 4 | -------------------------------------------------------------------------------- /dynamic-routes/next-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/dynamic-routes/next-10" 3 | } 4 | -------------------------------------------------------------------------------- /dynamic-routes/next-11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/dynamic-routes/next-11" 3 | } 4 | -------------------------------------------------------------------------------- /dynamic-routes/next-12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/dynamic-routes/next-12" 3 | } 4 | -------------------------------------------------------------------------------- /dynamic-routes/next-13/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../../dist/dynamic-routes/next-13" 3 | } 4 | -------------------------------------------------------------------------------- /dynamic-routes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/dynamic-routes/index" 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/w1/390hkp6x0g1dg5z5l50651pc0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | globals: { 64 | 'ts-jest': { 65 | diagnostics: false 66 | } 67 | }, 68 | 69 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 70 | // maxWorkers: "50%", 71 | 72 | // An array of directory names to be searched recursively up from the requiring module's location 73 | // moduleDirectories: [ 74 | // "node_modules" 75 | // ], 76 | 77 | // An array of file extensions your modules use 78 | // moduleFileExtensions: [ 79 | // "js", 80 | // "json", 81 | // "jsx", 82 | // "ts", 83 | // "tsx", 84 | // "node" 85 | // ], 86 | 87 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 88 | // moduleNameMapper: {}, 89 | 90 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 91 | // modulePathIgnorePatterns: [], 92 | 93 | // Activates notifications for test results 94 | // notify: false, 95 | 96 | // An enum that specifies notification mode. Requires { notify: true } 97 | // notifyMode: "failure-change", 98 | 99 | // A preset that is used as a base for Jest's configuration 100 | preset: "ts-jest", 101 | 102 | // Run tests from one or more projects 103 | // projects: undefined, 104 | 105 | // Use this configuration option to add custom reporters to Jest 106 | // reporters: undefined, 107 | 108 | // Automatically reset mock state between every test 109 | // resetMocks: false, 110 | 111 | // Reset the module registry before running each individual test 112 | // resetModules: false, 113 | 114 | // A path to a custom resolver 115 | // resolver: undefined, 116 | 117 | // Automatically restore mock state between every test 118 | // restoreMocks: false, 119 | 120 | // The root directory that Jest should scan for tests and modules within 121 | rootDir: "src", 122 | 123 | // A list of paths to directories that Jest should use to search for files in 124 | // roots: [ 125 | // "" 126 | // ], 127 | 128 | // Allows you to use a custom runner instead of Jest's default test runner 129 | // runner: "jest-runner", 130 | 131 | // The paths to modules that run some code to configure or set up the testing environment before each test 132 | // setupFiles: [], 133 | 134 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 135 | // setupFilesAfterEnv: [], 136 | 137 | // The number of seconds after which a test is considered as slow and reported as such in the results. 138 | // slowTestThreshold: 5, 139 | 140 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 141 | // snapshotSerializers: [], 142 | 143 | // The test environment that will be used for testing 144 | // testEnvironment: "jest-environment-jsdom", 145 | 146 | // Options that will be passed to the testEnvironment 147 | // testEnvironmentOptions: {}, 148 | 149 | // Adds a location field to test results 150 | // testLocationInResults: false, 151 | 152 | // The glob patterns Jest uses to detect test files 153 | // testMatch: [ 154 | // "**/__tests__/**/*.[jt]s?(x)", 155 | // "**/?(*.)+(spec|test).[tj]s?(x)" 156 | // ], 157 | 158 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 159 | // testPathIgnorePatterns: [ 160 | // "/node_modules/" 161 | // ], 162 | 163 | // The regexp pattern or array of patterns that Jest uses to detect test files 164 | // testRegex: [], 165 | 166 | // This option allows the use of a custom results processor 167 | // testResultsProcessor: undefined, 168 | 169 | // This option allows use of a custom test runner 170 | // testRunner: "jasmine2", 171 | 172 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 173 | // testURL: "http://localhost", 174 | 175 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 176 | // timers: "real", 177 | 178 | // A map from regular expressions to paths to transformers 179 | // transform: undefined, 180 | 181 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 182 | // transformIgnorePatterns: [ 183 | // "/node_modules/", 184 | // "\\.pnp\\.[^\\/]+$" 185 | // ], 186 | 187 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 188 | // unmockedModulePathPatterns: undefined, 189 | 190 | // Indicates whether each individual test should be reported during the run 191 | // verbose: undefined, 192 | 193 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 194 | // watchPathIgnorePatterns: [], 195 | 196 | // Whether to use watchman for file crawling 197 | // watchman: true, 198 | }; 199 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-router-mock", 3 | "version": "1.0.2", 4 | "description": "Mock implementation of the Next.js Router", 5 | "main": "dist/index", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./dist/index.js" 10 | }, 11 | "./*": { 12 | "types": "./dist/*/index.d.ts", 13 | "default": "./dist/*/index.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "files": [ 18 | "dist", 19 | "async", 20 | "dynamic-routes", 21 | "MemoryRouterProvider", 22 | "src" 23 | ], 24 | "scripts": { 25 | "checks": "npm run docs && npm run lint && npm run test && npm run build && npm run typecheck && npm run test:integration", 26 | "lint": "prettier --list-different src", 27 | "lint:fix": "prettier --write src", 28 | "docs": "doctoc README.md", 29 | "test": "jest", 30 | "test:watch": "jest --watch", 31 | "test:integration:ci": "npm run test:integration:install && npm run test:integration", 32 | "test:integration:install": "(cd test/example-app && npm i); (cd test/next-10 && npm i); (cd test/next-11 && npm i); (cd test/next-12 && npm i); (cd test/next-12.2+ && npm i); (cd test/next-12.latest && npm i); (cd test/next-13 && npm i); (cd test/next-13.5 && npm i)", 33 | "test:integration": "npm run build && npm run test:exports && npm run test:example-app && npm run test:next-10 && npm run test:next-11 && npm run test:next-12 && npm run test:next-12.2 && npm run test:next-12.latest && npm run test:next-13 && npm run test:next-13.5", 34 | "test:example-app": "cd test/example-app && npm test", 35 | "test:exports": "cd test/test-exports && npm test", 36 | "test:next-10": "cd test/next-10 && npm test", 37 | "test:next-11": "cd test/next-11 && npm test", 38 | "test:next-12": "cd test/next-12 && npm test", 39 | "test:next-12.2": "cd test/next-12.2+ && npm test", 40 | "test:next-12.latest": "cd test/next-12.latest && npm test", 41 | "test:next-13": "cd test/next-13 && npm test", 42 | "test:next-13.5": "cd test/next-13.5 && npm test", 43 | "typecheck": "tsc --noEmit", 44 | "build": "npm run clean && tsc --project tsconfig.build.json", 45 | "clean": "rimraf dist", 46 | "build:watch": "npm run build -- --watch", 47 | "prepublishOnly": "npm run build", 48 | "preversion": "npm run checks", 49 | "prerelease": "npm run build", 50 | "release": "changeset publish" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/scottrippey/next-router-mock.git" 55 | }, 56 | "keywords": [ 57 | "react", 58 | "next", 59 | "next.js", 60 | "nextjs", 61 | "router", 62 | "mock", 63 | "test", 64 | "testing" 65 | ], 66 | "author": "", 67 | "license": "MIT", 68 | "bugs": { 69 | "url": "https://github.com/scottrippey/next-router-mock/issues" 70 | }, 71 | "homepage": "https://github.com/scottrippey/next-router-mock#readme", 72 | "peerDependencies": { 73 | "next": ">=10.0.0", 74 | "react": ">=17.0.0" 75 | }, 76 | "devDependencies": { 77 | "@changesets/cli": "^2.26.2", 78 | "@testing-library/react": "^13.4.0", 79 | "@types/jest": "^26.0.20", 80 | "doctoc": "^2.2.0", 81 | "jest": "^26.6.3", 82 | "next": "^13.5.1", 83 | "prettier": "^2.2.1", 84 | "react": "^18.2.0", 85 | "react-dom": "^18.2.0", 86 | "react-test-renderer": "^18.2.0", 87 | "rimraf": "^3.0.2", 88 | "ts-jest": "^26.4.4", 89 | "typescript": "^4.9.5" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/MemoryRouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from "./MemoryRouter"; 2 | import { expectMatch } from "../test/test-utils"; 3 | 4 | describe("MemoryRouter", () => { 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | }); 8 | 9 | [ 10 | // Test in both sync and async modes: 11 | { async: false }, 12 | { async: true }, 13 | ].forEach(({ async }) => { 14 | describe(async ? "async mode" : "sync mode", () => { 15 | const memoryRouter = new MemoryRouter(); 16 | memoryRouter.async = async; 17 | 18 | it("should start empty", async () => { 19 | expectMatch(memoryRouter, { 20 | asPath: "/", 21 | pathname: "/", 22 | route: "/", 23 | query: {}, 24 | locale: undefined, 25 | }); 26 | }); 27 | it("pushing URLs should update the route", async () => { 28 | await memoryRouter.push("/one/two/three"); 29 | 30 | expectMatch(memoryRouter, { 31 | asPath: "/one/two/three", 32 | pathname: "/one/two/three", 33 | route: "/one/two/three", 34 | query: {}, 35 | }); 36 | 37 | await memoryRouter.push("/one/two/three?four=4&five="); 38 | 39 | expectMatch(memoryRouter, { 40 | asPath: "/one/two/three?four=4&five=", 41 | pathname: "/one/two/three", 42 | query: { 43 | five: "", 44 | four: "4", 45 | }, 46 | }); 47 | }); 48 | 49 | describe("events: routeChange and hashChange", () => { 50 | const routeChangeStart = jest.fn(); 51 | const routeChangeComplete = jest.fn(); 52 | const hashChangeStart = jest.fn(); 53 | const hashChangeComplete = jest.fn(); 54 | beforeAll(() => { 55 | memoryRouter.events.on("routeChangeStart", routeChangeStart); 56 | memoryRouter.events.on("routeChangeComplete", routeChangeComplete); 57 | memoryRouter.events.on("hashChangeStart", hashChangeStart); 58 | memoryRouter.events.on("hashChangeComplete", hashChangeComplete); 59 | }); 60 | afterAll(() => { 61 | memoryRouter.events.off("routeChangeStart", routeChangeStart); 62 | memoryRouter.events.off("routeChangeComplete", routeChangeComplete); 63 | memoryRouter.events.off("hashChangeStart", hashChangeStart); 64 | memoryRouter.events.off("hashChangeComplete", hashChangeComplete); 65 | }); 66 | 67 | it("should both be triggered when pushing a URL", async () => { 68 | await memoryRouter.push("/one"); 69 | expect(routeChangeStart).toHaveBeenCalledWith("/one", { 70 | shallow: false, 71 | }); 72 | expect(routeChangeComplete).toHaveBeenCalledWith("/one", { 73 | shallow: false, 74 | }); 75 | }); 76 | 77 | it("should trigger only hashEvents for /baz -> /baz#foo", async () => { 78 | await memoryRouter.push("/baz"); 79 | jest.clearAllMocks(); 80 | await memoryRouter.push("/baz#foo"); 81 | expect(hashChangeStart).toHaveBeenCalledWith("/baz#foo", { shallow: false }); 82 | expect(hashChangeComplete).toHaveBeenCalledWith("/baz#foo", { shallow: false }); 83 | expect(routeChangeStart).not.toHaveBeenCalled(); 84 | expect(routeChangeComplete).not.toHaveBeenCalled(); 85 | }); 86 | 87 | it("should trigger only hashEvents for /baz#foo -> /baz#foo", async () => { 88 | await memoryRouter.push("/baz#foo"); 89 | jest.clearAllMocks(); 90 | await memoryRouter.push("/baz#foo"); 91 | expect(hashChangeStart).toHaveBeenCalledWith("/baz#foo", { shallow: false }); 92 | expect(hashChangeComplete).toHaveBeenCalledWith("/baz#foo", { shallow: false }); 93 | expect(routeChangeStart).not.toHaveBeenCalled(); 94 | expect(routeChangeComplete).not.toHaveBeenCalled(); 95 | }); 96 | 97 | it("should trigger only hashEvents for /baz#foo -> /baz#bar", async () => { 98 | await memoryRouter.push("/baz#foo"); 99 | jest.clearAllMocks(); 100 | await memoryRouter.push("/baz#bar"); 101 | expect(hashChangeStart).toHaveBeenCalledWith("/baz#bar", { shallow: false }); 102 | expect(hashChangeComplete).toHaveBeenCalledWith("/baz#bar", { shallow: false }); 103 | expect(routeChangeStart).not.toHaveBeenCalled(); 104 | expect(routeChangeComplete).not.toHaveBeenCalled(); 105 | }); 106 | 107 | it("should trigger only hashEvents for /baz#foo -> /baz", async () => { 108 | await memoryRouter.push("/baz#foo"); 109 | jest.clearAllMocks(); 110 | await memoryRouter.push("/baz"); 111 | expect(hashChangeStart).toHaveBeenCalledWith("/baz", { shallow: false }); 112 | expect(hashChangeComplete).toHaveBeenCalledWith("/baz", { shallow: false }); 113 | expect(routeChangeStart).not.toHaveBeenCalled(); 114 | expect(routeChangeComplete).not.toHaveBeenCalled(); 115 | }); 116 | 117 | it("should trigger only routeEvents for /baz -> /baz", async () => { 118 | await memoryRouter.push("/baz"); 119 | jest.clearAllMocks(); 120 | await memoryRouter.push("/baz"); 121 | expect(hashChangeStart).not.toHaveBeenCalled(); 122 | expect(hashChangeComplete).not.toHaveBeenCalled(); 123 | expect(routeChangeStart).toHaveBeenCalledWith("/baz", { shallow: false }); 124 | expect(routeChangeComplete).toHaveBeenCalledWith("/baz", { shallow: false }); 125 | }); 126 | 127 | it("should trigger only routeEvents for /baz -> /foo#baz", async () => { 128 | await memoryRouter.push("/baz"); 129 | jest.clearAllMocks(); 130 | await memoryRouter.push("/foo#baz"); 131 | expect(hashChangeStart).not.toHaveBeenCalled(); 132 | expect(hashChangeComplete).not.toHaveBeenCalled(); 133 | expect(routeChangeStart).toHaveBeenCalledWith("/foo#baz", { shallow: false }); 134 | expect(routeChangeComplete).toHaveBeenCalledWith("/foo#baz", { shallow: false }); 135 | }); 136 | 137 | if (async) { 138 | it("routeChange events should be triggered in the correct async order", async () => { 139 | const promise = memoryRouter.push("/one/two/three"); 140 | expect(routeChangeStart).toHaveBeenCalledWith("/one/two/three", { 141 | shallow: false, 142 | }); 143 | expect(routeChangeComplete).not.toHaveBeenCalled(); 144 | await promise; 145 | expect(routeChangeComplete).toHaveBeenCalledWith("/one/two/three", { 146 | shallow: false, 147 | }); 148 | }); 149 | it("hashChange events should be triggered in the correct async order", async () => { 150 | await memoryRouter.push("/baz"); 151 | jest.clearAllMocks(); 152 | const promise = memoryRouter.push("/baz#foo"); 153 | expect(hashChangeStart).toHaveBeenCalledWith("/baz#foo", { 154 | shallow: false, 155 | }); 156 | expect(hashChangeComplete).not.toHaveBeenCalled(); 157 | await promise; 158 | expect(hashChangeComplete).toHaveBeenCalledWith("/baz#foo", { 159 | shallow: false, 160 | }); 161 | }); 162 | } 163 | 164 | it("should be triggered when pushing a URL Object", async () => { 165 | await memoryRouter.push({ 166 | pathname: "/one/two", 167 | query: { foo: "bar" }, 168 | }); 169 | expect(routeChangeStart).toHaveBeenCalled(); 170 | expect(routeChangeStart).toHaveBeenCalledWith("/one/two?foo=bar", { 171 | shallow: false, 172 | }); 173 | expect(routeChangeComplete).toHaveBeenCalledWith("/one/two?foo=bar", { 174 | shallow: false, 175 | }); 176 | }); 177 | 178 | it("should be triggered when replacing", async () => { 179 | await memoryRouter.replace("/one/two/three"); 180 | expect(routeChangeStart).toHaveBeenCalled(); 181 | expect(routeChangeStart).toHaveBeenCalledWith("/one/two/three", { 182 | shallow: false, 183 | }); 184 | expect(routeChangeComplete).toHaveBeenCalledWith("/one/two/three", { 185 | shallow: false, 186 | }); 187 | }); 188 | 189 | it('should provide the "shallow" value', async () => { 190 | await memoryRouter.push("/test", undefined, { shallow: true }); 191 | expect(routeChangeStart).toHaveBeenCalled(); 192 | expect(routeChangeStart).toHaveBeenCalledWith("/test", { 193 | shallow: true, 194 | }); 195 | expect(routeChangeComplete).toHaveBeenCalledWith("/test", { 196 | shallow: true, 197 | }); 198 | }); 199 | }); 200 | 201 | it("pushing UrlObjects should update the route", async () => { 202 | await memoryRouter.push({ pathname: "/one" }); 203 | expectMatch(memoryRouter, { 204 | asPath: "/one", 205 | pathname: "/one", 206 | query: {}, 207 | }); 208 | 209 | await memoryRouter.push({ 210 | pathname: "/one/two/three", 211 | query: { four: "4", five: "" }, 212 | }); 213 | expectMatch(memoryRouter, { 214 | asPath: "/one/two/three?four=4&five=", 215 | pathname: "/one/two/three", 216 | query: { 217 | five: "", 218 | four: "4", 219 | }, 220 | }); 221 | }); 222 | it("pushing UrlObjects should inject slugs", async () => { 223 | await memoryRouter.push({ 224 | pathname: "/one/[id]", 225 | query: { id: "two" }, 226 | }); 227 | expectMatch(memoryRouter, { 228 | asPath: "/one/two", 229 | pathname: "/one/[id]", 230 | query: { 231 | id: "two", 232 | }, 233 | }); 234 | 235 | await memoryRouter.push({ 236 | pathname: "/one/[id]/three", 237 | query: { id: "two" }, 238 | }); 239 | expectMatch(memoryRouter, { 240 | asPath: "/one/two/three", 241 | pathname: "/one/[id]/three", 242 | query: { 243 | id: "two", 244 | }, 245 | }); 246 | 247 | await memoryRouter.push({ 248 | pathname: "/one/[id]/three", 249 | query: { id: "two", four: "4" }, 250 | }); 251 | expectMatch(memoryRouter, { 252 | asPath: "/one/two/three?four=4", 253 | pathname: "/one/[id]/three", 254 | query: { 255 | four: "4", 256 | id: "two", 257 | }, 258 | }); 259 | await memoryRouter.push({ 260 | pathname: "/one/[id]/three/[four]", 261 | query: { id: "two", four: "4" }, 262 | }); 263 | expectMatch(memoryRouter, { 264 | asPath: "/one/two/three/4", 265 | pathname: "/one/[id]/three/[four]", 266 | query: { 267 | four: "4", 268 | id: "two", 269 | }, 270 | }); 271 | await memoryRouter.push({ 272 | pathname: "/one/[...slug]", 273 | query: { slug: ["two", "three", "four"], filter: "abc" }, 274 | }); 275 | expectMatch(memoryRouter, { 276 | asPath: "/one/two/three/four?filter=abc", 277 | pathname: "/one/[...slug]", 278 | query: { 279 | slug: ["two", "three", "four"], 280 | filter: "abc", 281 | }, 282 | }); 283 | await memoryRouter.push({ 284 | pathname: "/one/two/[[...slug]]", 285 | query: { slug: ["three", "four"] }, 286 | }); 287 | expectMatch(memoryRouter, { 288 | asPath: "/one/two/three/four", 289 | pathname: "/one/two/[[...slug]]", 290 | query: { slug: ["three", "four"] }, 291 | }); 292 | await memoryRouter.push({ 293 | pathname: "/one/two/[[...slug]]", 294 | query: {}, 295 | }); 296 | expectMatch(memoryRouter, { 297 | asPath: "/one/two", 298 | pathname: "/one/two/[[...slug]]", 299 | query: {}, 300 | }); 301 | }); 302 | it("push the locale", async () => { 303 | await memoryRouter.push("/", undefined, { locale: "en" }); 304 | expectMatch(memoryRouter, { 305 | locale: "en", 306 | }); 307 | }); 308 | 309 | it("should support the locales property", async () => { 310 | expect(memoryRouter.locales).toEqual([]); 311 | memoryRouter.locales = ["en", "fr"]; 312 | expect(memoryRouter.locales).toEqual(["en", "fr"]); 313 | }); 314 | 315 | it("prefetch should do nothing", async () => { 316 | expect(await memoryRouter.prefetch()).toBeUndefined(); 317 | }); 318 | 319 | it("trailing slashes are normalized", async () => { 320 | memoryRouter.setCurrentUrl("/path/"); 321 | expectMatch(memoryRouter, { 322 | asPath: "/path", 323 | pathname: "/path", 324 | }); 325 | 326 | memoryRouter.setCurrentUrl(""); 327 | expectMatch(memoryRouter, { 328 | asPath: "/", 329 | pathname: "/", 330 | }); 331 | }); 332 | 333 | it("a single slash is preserved", async () => { 334 | memoryRouter.setCurrentUrl(""); 335 | expectMatch(memoryRouter, { 336 | asPath: "/", 337 | pathname: "/", 338 | }); 339 | 340 | memoryRouter.setCurrentUrl("/"); 341 | expectMatch(memoryRouter, { 342 | asPath: "/", 343 | pathname: "/", 344 | }); 345 | }); 346 | 347 | it("multiple values can be specified for a query parameter", () => { 348 | memoryRouter.setCurrentUrl("/url?foo=FOO&foo=BAR"); 349 | expectMatch(memoryRouter, { 350 | asPath: "/url?foo=FOO&foo=BAR", 351 | query: { 352 | foo: ["FOO", "BAR"], 353 | }, 354 | }); 355 | 356 | memoryRouter.setCurrentUrl({ pathname: "/object-notation", query: { foo: ["BAR", "BAZ"] } }); 357 | expectMatch(memoryRouter, { 358 | asPath: "/object-notation?foo=BAR&foo=BAZ", 359 | query: { foo: ["BAR", "BAZ"] }, 360 | }); 361 | }); 362 | 363 | describe('the "as" parameter', () => { 364 | it("works with strings or objects", async () => { 365 | await memoryRouter.push("/path", "/path?param=as"); 366 | expectMatch(memoryRouter, { 367 | asPath: "/path?param=as", 368 | pathname: "/path", 369 | query: {}, 370 | }); 371 | 372 | await memoryRouter.push("/path", { pathname: "/path", query: { param: "as" } }); 373 | expectMatch(memoryRouter, { 374 | asPath: "/path?param=as", 375 | pathname: "/path", 376 | query: {}, 377 | }); 378 | }); 379 | 380 | it("the real query is always used", async () => { 381 | await memoryRouter.push("/path?queryParam=123", "/path"); 382 | expectMatch(memoryRouter, { 383 | asPath: "/path", 384 | pathname: "/path", 385 | query: { queryParam: "123" }, 386 | }); 387 | 388 | await memoryRouter.push("/path", "/path?queryParam=123"); 389 | expectMatch(memoryRouter, { 390 | asPath: "/path?queryParam=123", 391 | pathname: "/path", 392 | query: {}, 393 | }); 394 | 395 | await memoryRouter.push("/path?queryParam=123", "/path?differentQueryParam=456"); 396 | expectMatch(memoryRouter, { 397 | asPath: "/path?differentQueryParam=456", 398 | pathname: "/path", 399 | query: { queryParam: "123" }, 400 | }); 401 | 402 | await memoryRouter.push("/path?queryParam=123", { 403 | pathname: "/path", 404 | query: { differentQueryParam: "456" }, 405 | }); 406 | expectMatch(memoryRouter, { 407 | asPath: "/path?differentQueryParam=456", 408 | pathname: "/path", 409 | query: { queryParam: "123" }, 410 | }); 411 | 412 | await memoryRouter.push({ pathname: "", query: { queryParam: "123" } }, ""); 413 | expectMatch(memoryRouter, { 414 | asPath: "/", 415 | pathname: "/", 416 | query: { queryParam: "123" }, 417 | }); 418 | }); 419 | 420 | describe("search parameter", () => { 421 | it("happy path", async () => { 422 | await memoryRouter.push({ 423 | pathname: "/path", 424 | search: "foo=FOO&bar=BAR", 425 | }); 426 | expectMatch(memoryRouter, { 427 | asPath: "/path?foo=FOO&bar=BAR", 428 | pathname: "/path", 429 | query: { 430 | foo: "FOO", 431 | bar: "BAR", 432 | }, 433 | }); 434 | }); 435 | 436 | it("multiple values can be specified for a query parameter", async () => { 437 | await memoryRouter.push({ 438 | pathname: "/path", 439 | search: "foo=FOO&foo=BAR", 440 | }); 441 | expectMatch(memoryRouter, { 442 | asPath: "/path?foo=FOO&foo=BAR", 443 | pathname: "/path", 444 | query: { 445 | foo: ["FOO", "BAR"], 446 | }, 447 | }); 448 | }); 449 | 450 | it("if search and query are both provided preference is given to search", async () => { 451 | await memoryRouter.push({ 452 | pathname: "/path", 453 | search: "foo=FOO&bar=BAR", 454 | query: { 455 | baz: "BAZ", 456 | }, 457 | }); 458 | expectMatch(memoryRouter, { 459 | asPath: "/path?foo=FOO&bar=BAR", 460 | pathname: "/path", 461 | query: { 462 | foo: "FOO", 463 | bar: "BAR", 464 | }, 465 | }); 466 | }); 467 | }); 468 | 469 | describe("with different paths", () => { 470 | it("the real path and query are used", async () => { 471 | await memoryRouter.push("/real-path", "/as-path"); 472 | expectMatch(memoryRouter, { 473 | asPath: "/as-path", 474 | pathname: "/real-path", 475 | query: {}, 476 | }); 477 | 478 | await memoryRouter.push("/real-path?real=real", "/as-path?as=as"); 479 | expectMatch(memoryRouter, { 480 | asPath: "/as-path?as=as", 481 | pathname: "/real-path", 482 | query: { real: "real" }, 483 | }); 484 | 485 | await memoryRouter.push("/real-path?param=real", "/as-path?param=as"); 486 | expectMatch(memoryRouter, { 487 | asPath: "/as-path?param=as", 488 | pathname: "/real-path", 489 | query: { param: "real" }, 490 | }); 491 | }); 492 | }); 493 | 494 | it('"as" param hash overrides "url" hash', async () => { 495 | await memoryRouter.push("/path", "/path#as-hash"); 496 | expectMatch(memoryRouter, { 497 | asPath: "/path#as-hash", 498 | pathname: "/path", 499 | hash: "#as-hash", 500 | }); 501 | 502 | await memoryRouter.push("/path", { pathname: "/path", hash: "#as-hash" }); 503 | expectMatch(memoryRouter, { 504 | asPath: "/path#as-hash", 505 | pathname: "/path", 506 | hash: "#as-hash", 507 | }); 508 | 509 | await memoryRouter.push("/path#real-hash", "/path#as-hash"); 510 | expectMatch(memoryRouter, { 511 | asPath: "/path#as-hash", 512 | pathname: "/path", 513 | hash: "#as-hash", 514 | }); 515 | 516 | await memoryRouter.push("/path", { pathname: "/path", hash: "#as-hash" }); 517 | expectMatch(memoryRouter, { asPath: "/path#as-hash", pathname: "/path", hash: "#as-hash" }); 518 | 519 | await memoryRouter.push("/path#real-hash", "/path"); 520 | expectMatch(memoryRouter, { asPath: "/path", pathname: "/path", hash: "" }); 521 | 522 | await memoryRouter.push("/path", { pathname: "/path" }); 523 | expectMatch(memoryRouter, { asPath: "/path", pathname: "/path", hash: "" }); 524 | 525 | await memoryRouter.push("/path#real-hash", "/as-path"); 526 | expectMatch(memoryRouter, { 527 | asPath: "/as-path", 528 | pathname: "/path", 529 | hash: "", 530 | }); 531 | 532 | await memoryRouter.push("/path", { pathname: "/as-path" }); 533 | expectMatch(memoryRouter, { 534 | asPath: "/as-path", 535 | pathname: "/path", 536 | hash: "", 537 | }); 538 | 539 | await memoryRouter.push("/path#real-hash", "/as-path#as-hash"); 540 | expectMatch(memoryRouter, { 541 | asPath: "/as-path#as-hash", 542 | pathname: "/path", 543 | hash: "#as-hash", 544 | }); 545 | 546 | await memoryRouter.push("/path", { pathname: "/as-path", hash: "#as-hash" }); 547 | expectMatch(memoryRouter, { 548 | asPath: "/as-path#as-hash", 549 | pathname: "/path", 550 | hash: "#as-hash", 551 | }); 552 | }); 553 | }); 554 | 555 | it("should allow deconstruction of push and replace", async () => { 556 | const { push, replace } = memoryRouter; 557 | await push("/one"); 558 | expectMatch(memoryRouter, { asPath: "/one" }); 559 | await replace("/two"); 560 | expectMatch(memoryRouter, { asPath: "/two" }); 561 | }); 562 | 563 | it("should allow push with no path, just a query", async () => { 564 | await memoryRouter.push("/path"); 565 | 566 | await memoryRouter.push({ query: { id: "42" } }); 567 | 568 | expect(memoryRouter.asPath).toEqual("/path?id=42"); 569 | }); 570 | 571 | it("hashes are preserved", async () => { 572 | memoryRouter.setCurrentUrl("/path#hash"); 573 | expectMatch(memoryRouter, { 574 | asPath: "/path#hash", 575 | pathname: "/path", 576 | hash: "#hash", 577 | }); 578 | 579 | memoryRouter.setCurrentUrl("/path?key=value#hash"); 580 | expectMatch(memoryRouter, { 581 | asPath: "/path?key=value#hash", 582 | pathname: "/path", 583 | query: { key: "value" }, 584 | hash: "#hash", 585 | }); 586 | }); 587 | }); 588 | }); 589 | }); 590 | -------------------------------------------------------------------------------- /src/MemoryRouter.tsx: -------------------------------------------------------------------------------- 1 | import type { NextRouter, RouterEvent } from "next/router"; 2 | import mitt, { MittEmitter } from "./lib/mitt"; 3 | import { parseUrl, parseQueryString, stringifyQueryString } from "./urls"; 4 | 5 | export type Url = string | UrlObject; 6 | export type UrlObject = { 7 | pathname?: string | null | undefined; 8 | query?: NextRouter["query"]; 9 | hash?: string; 10 | search?: string; 11 | }; 12 | export type UrlObjectComplete = { 13 | pathname: string; 14 | query: NextRouter["query"]; 15 | hash: string; 16 | // While parsing, keep the routeParams separate from the query. 17 | // We'll merge them at the end. 18 | routeParams: NextRouter["query"]; 19 | }; 20 | 21 | // interface not exported by the package next/router 22 | interface TransitionOptions { 23 | shallow?: boolean; 24 | locale?: string | false; 25 | scroll?: boolean; 26 | } 27 | 28 | type InternalEventTypes = 29 | /** Allows custom parsing logic */ 30 | | "NEXT_ROUTER_MOCK:parse" 31 | /** Emitted when 'router.push' is called */ 32 | | "NEXT_ROUTER_MOCK:push" 33 | /** Emitted when 'router.replace' is called */ 34 | | "NEXT_ROUTER_MOCK:replace"; 35 | 36 | /** 37 | * A base implementation of NextRouter that does nothing; all methods throw. 38 | */ 39 | export abstract class BaseRouter implements NextRouter { 40 | pathname = "/"; 41 | query: NextRouter["query"] = {}; 42 | asPath = "/"; 43 | /** 44 | * The `hash` property is NOT part of NextRouter. 45 | * It is only supplied as part of next-router-mock, for the sake of testing 46 | */ 47 | hash = ""; 48 | 49 | // These are constant: 50 | isReady = true; 51 | basePath = ""; 52 | isFallback = false; 53 | isPreview = false; 54 | isLocaleDomain = false; 55 | locale: NextRouter["locale"] = undefined; 56 | locales: NextRouter["locales"] = []; 57 | defaultLocale?: NextRouter["defaultLocale"]; 58 | domainLocales?: NextRouter["domainLocales"]; 59 | 60 | events: MittEmitter = mitt(); 61 | 62 | abstract push(url: Url, as?: Url, options?: TransitionOptions): Promise; 63 | abstract replace(url: Url): Promise; 64 | back() { 65 | // Not implemented 66 | } 67 | forward() { 68 | // Not implemented 69 | } 70 | beforePopState() { 71 | // Do nothing 72 | } 73 | async prefetch(): Promise { 74 | // Do nothing 75 | } 76 | reload() { 77 | // Do nothing 78 | } 79 | 80 | // Keep route and pathname values in sync 81 | get route() { 82 | return this.pathname; 83 | } 84 | } 85 | 86 | export type MemoryRouterSnapshot = Readonly; 87 | 88 | /** 89 | * An implementation of NextRouter that does not change the URL, but just stores the current route in memory. 90 | */ 91 | export class MemoryRouter extends BaseRouter { 92 | static snapshot(original: MemoryRouter): MemoryRouterSnapshot { 93 | return Object.assign(new MemoryRouter(), original); 94 | } 95 | 96 | constructor(initialUrl?: Url, async?: boolean) { 97 | super(); 98 | if (initialUrl) this.setCurrentUrl(initialUrl); 99 | if (async) this.async = async; 100 | } 101 | 102 | /** 103 | * When enabled, there will be a short delay between calling `push` and when the router is updated. 104 | * This is used to simulate Next's async behavior. 105 | * However, for most tests, it is more convenient to leave this off. 106 | */ 107 | public async = false; 108 | 109 | /** 110 | * Store extra metadata, needed to support App Router (next/navigation) 111 | */ 112 | public internal = { 113 | query: {} as NextRouter["query"], 114 | routeParams: {} as NextRouter["query"], 115 | selectedLayoutSegment: "[next-router-mock] Not Yet Implemented", 116 | selectedLayoutSegments: ["[next-router-mock] Not Yet Implemented"], 117 | }; 118 | 119 | /** 120 | * Removes all event handlers, and sets the current URL back to default. 121 | * This will clear dynamic parsers, too. 122 | */ 123 | public reset() { 124 | this.events = mitt(); 125 | this.setCurrentUrl("/"); 126 | } 127 | 128 | public useParser(parser: (urlObject: UrlObjectComplete) => void) { 129 | this.events.on("NEXT_ROUTER_MOCK:parse", parser); 130 | return () => this.events.off("NEXT_ROUTER_MOCK:parse", parser); 131 | } 132 | 133 | push = (url: Url, as?: Url, options?: TransitionOptions) => { 134 | return this._setCurrentUrl(url, as, options, "push"); 135 | }; 136 | 137 | replace = (url: Url, as?: Url, options?: TransitionOptions) => { 138 | return this._setCurrentUrl(url, as, options, "replace"); 139 | }; 140 | 141 | /** 142 | * Sets the current Memory route to the specified url, synchronously. 143 | */ 144 | public setCurrentUrl = (url: Url, as?: Url) => { 145 | // (ignore the returned promise) 146 | void this._setCurrentUrl(url, as, undefined, "set", false); 147 | }; 148 | 149 | private async _setCurrentUrl( 150 | url: Url, 151 | as?: Url, 152 | options?: TransitionOptions, 153 | source?: "push" | "replace" | "set", 154 | async = this.async 155 | ) { 156 | // Parse the URL if needed: 157 | const newRoute = parseUrlToCompleteUrl(url, this.pathname); 158 | // Optionally apply dynamic routes (can mutate routes) 159 | this.events.emit("NEXT_ROUTER_MOCK:parse", newRoute); 160 | 161 | let asPath: string; 162 | if (as === undefined || as === null) { 163 | asPath = getRouteAsPath(newRoute); 164 | } else { 165 | const asRoute = parseUrlToCompleteUrl(as, this.pathname); 166 | this.events.emit("NEXT_ROUTER_MOCK:parse", asRoute); 167 | 168 | asPath = getRouteAsPath(asRoute); 169 | 170 | // "as" hash and route params always take precedence: 171 | newRoute.hash = asRoute.hash; 172 | newRoute.routeParams = asRoute.routeParams; 173 | } 174 | 175 | const shallow = options?.shallow || false; 176 | 177 | // Fire "start" event: 178 | const triggerHashChange = shouldTriggerHashChange(this, newRoute); 179 | if (triggerHashChange) { 180 | this.events.emit("hashChangeStart", asPath, { shallow }); 181 | } else { 182 | this.events.emit("routeChangeStart", asPath, { shallow }); 183 | } 184 | 185 | // Simulate the async nature of this method 186 | if (async) await new Promise((resolve) => setTimeout(resolve, 0)); 187 | 188 | // Update this instance: 189 | this.asPath = asPath; 190 | this.pathname = newRoute.pathname; 191 | this.query = { ...newRoute.query, ...newRoute.routeParams }; 192 | this.hash = newRoute.hash; 193 | this.internal.query = newRoute.query; 194 | this.internal.routeParams = newRoute.routeParams; 195 | 196 | if (options?.locale) { 197 | this.locale = options.locale; 198 | } 199 | 200 | // Fire "complete" event: 201 | if (triggerHashChange) { 202 | this.events.emit("hashChangeComplete", this.asPath, { shallow }); 203 | } else { 204 | this.events.emit("routeChangeComplete", this.asPath, { shallow }); 205 | } 206 | 207 | // Fire internal events: 208 | const eventName = 209 | source === "push" ? "NEXT_ROUTER_MOCK:push" : source === "replace" ? "NEXT_ROUTER_MOCK:replace" : undefined; 210 | if (eventName) this.events.emit(eventName, this.asPath, { shallow }); 211 | 212 | return true; 213 | } 214 | } 215 | 216 | /** 217 | * Normalizes the url or urlObject into a UrlObjectComplete. 218 | */ 219 | function parseUrlToCompleteUrl(url: Url, currentPathname: string): UrlObjectComplete { 220 | const parsedUrl = typeof url === "object" ? url : parseUrl(url); 221 | 222 | const queryFromSearch = parsedUrl.search ? parseQueryString(parsedUrl.search) : undefined; 223 | const query = queryFromSearch ?? parsedUrl.query ?? {}; 224 | 225 | return { 226 | pathname: normalizeTrailingSlash(parsedUrl.pathname ?? currentPathname), 227 | query, 228 | hash: parsedUrl.hash || "", 229 | routeParams: {}, 230 | }; 231 | } 232 | 233 | /** 234 | * Creates a URL from a pathname + query. 235 | * Injects query params into the URL slugs, the same way that next/router does. 236 | */ 237 | function getRouteAsPath({ pathname, query, hash, routeParams }: UrlObjectComplete) { 238 | const remainingQuery = { ...query }; 239 | 240 | // Replace slugs, and remove them from the `query` 241 | let asPath = pathname.replace(/\[{1,2}(.+?)]{1,2}/g, ($0, slug: string) => { 242 | if (slug.startsWith("...")) slug = slug.replace("...", ""); 243 | 244 | let value = routeParams[slug]; 245 | if (!value) { 246 | // Pop the slug value from the query: 247 | value = remainingQuery[slug]!; 248 | delete remainingQuery[slug]; 249 | } 250 | 251 | if (Array.isArray(value)) { 252 | return value.map((v) => encodeURIComponent(v)).join("/"); 253 | } 254 | return value !== undefined ? encodeURIComponent(String(value)) : ""; 255 | }); 256 | 257 | // Remove any trailing slashes; this can occur if there is no match for a catch-all slug ([[...slug]]) 258 | asPath = normalizeTrailingSlash(asPath); 259 | 260 | // Append remaining query as a querystring, if needed: 261 | const qs = stringifyQueryString(remainingQuery); 262 | 263 | if (qs) asPath += `?${qs}`; 264 | if (hash) asPath += hash; 265 | 266 | return asPath; 267 | } 268 | 269 | function normalizeTrailingSlash(path: string) { 270 | return path.endsWith("/") && path.length > 1 ? path.slice(0, -1) : path || "/"; 271 | } 272 | 273 | function shouldTriggerHashChange(current: MemoryRouter, newRoute: Pick) { 274 | const isHashChange = current.hash !== newRoute.hash; 275 | const isQueryChange = stringifyQueryString(current.query) !== stringifyQueryString(newRoute.query); 276 | const isRouteChange = isQueryChange || current.pathname !== newRoute.pathname; 277 | 278 | /** 279 | * Try to replicate NextJs routing behaviour: 280 | * 281 | * /foo -> routeChange 282 | * /foo#baz -> hashChange 283 | * /foo#baz -> hashChange 284 | * /foo -> hashChange 285 | * /foo -> routeChange 286 | * /bar#fuz -> routeChange 287 | */ 288 | return !isRouteChange && (isHashChange || newRoute.hash); 289 | } 290 | -------------------------------------------------------------------------------- /src/MemoryRouterContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MemoryRouterSnapshot } from "./MemoryRouter"; 3 | 4 | /** 5 | * This context is optionally used by 6 | */ 7 | export const MemoryRouterContext = React.createContext(null); 8 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/MemoryRouterProvider.async.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import NextLink, { LinkProps } from "next/link"; 4 | import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; 5 | 6 | import { MemoryRouterProvider } from "./index"; 7 | import { default as singletonRouter } from "../async"; 8 | 9 | const TestLink = (linkProps: Partial) => { 10 | const router = useRouter(); 11 | return ( 12 | 13 | Current route: "{router.asPath}" 14 | 15 | ); 16 | }; 17 | 18 | describe("MemoryRouterProvider", () => { 19 | beforeEach(() => { 20 | singletonRouter.setCurrentUrl("/initial"); 21 | }); 22 | 23 | it("should provide a router", () => { 24 | render( 25 | 26 | 27 | 28 | ); 29 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 30 | }); 31 | 32 | it("using the singleton router should update the URL", async () => { 33 | render( 34 | 35 | 36 | 37 | ); 38 | 39 | // Navigate: 40 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 41 | await act(async () => await singletonRouter.push("/new-route")); 42 | await waitFor(() => expect(screen.getByText(`Current route: "/new-route"`)).toBeDefined()); 43 | }); 44 | 45 | it("clicking a link should navigate to the new URL", async () => { 46 | render( 47 | 48 | 49 | 50 | ); 51 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 52 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 53 | await waitFor(() => expect(screen.getByText(`Current route: "/test"`)).toBeDefined()); 54 | }); 55 | 56 | describe("url", () => { 57 | it("an initial URL can be supplied", () => { 58 | render( 59 | 60 | 61 | 62 | ); 63 | expect(screen.getByText(`Current route: "/example"`)).toBeDefined(); 64 | }); 65 | 66 | it("an initial URL Object can be supplied", () => { 67 | render( 68 | 69 | 70 | 71 | ); 72 | expect(screen.getByText(`Current route: "/example?foo=bar"`)).toBeDefined(); 73 | }); 74 | }); 75 | 76 | describe("events", () => { 77 | const eventHandlers = { 78 | onPush: jest.fn(), 79 | onReplace: jest.fn(), 80 | onRouteChangeStart: jest.fn(), 81 | onRouteChangeComplete: jest.fn(), 82 | }; 83 | beforeEach(async () => { 84 | jest.clearAllMocks(); 85 | }); 86 | it("clicking a link should trigger the correct event handlers", async () => { 87 | render( 88 | 89 | 90 | 91 | ); 92 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 93 | await waitFor(() => expect(screen.getByText(`Current route: "/test"`)).not.toBeNull()); 94 | expect(eventHandlers.onPush).toHaveBeenCalledWith("/test", { shallow: false }); 95 | expect(eventHandlers.onReplace).not.toHaveBeenCalled(); 96 | expect(eventHandlers.onRouteChangeStart).toHaveBeenCalledWith("/test", { shallow: false }); 97 | expect(eventHandlers.onRouteChangeComplete).toHaveBeenCalledWith("/test", { shallow: false }); 98 | }); 99 | it("a 'replace' link triggers the correct handlers too", async () => { 100 | render( 101 | 102 | 103 | 104 | ); 105 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 106 | await waitFor(() => expect(screen.getByText(`Current route: "/test"`)).not.toBeNull()); 107 | expect(eventHandlers.onPush).not.toHaveBeenCalledWith("/test", { shallow: false }); 108 | expect(eventHandlers.onReplace).toHaveBeenCalled(); 109 | expect(eventHandlers.onRouteChangeStart).toHaveBeenCalledWith("/test", { shallow: false }); 110 | expect(eventHandlers.onRouteChangeComplete).toHaveBeenCalledWith("/test", { shallow: false }); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/MemoryRouterProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | import NextLink, { LinkProps } from "next/link"; 4 | import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; 5 | 6 | import { MemoryRouterProvider } from "./index"; 7 | import { default as singletonRouter } from "../index"; 8 | 9 | const TestLink = (linkProps: Partial) => { 10 | const router = useRouter(); 11 | return ( 12 | 13 | Current route: "{router.asPath}" 14 | 15 | ); 16 | }; 17 | 18 | describe("MemoryRouterProvider", () => { 19 | beforeEach(() => { 20 | singletonRouter.setCurrentUrl("/initial"); 21 | }); 22 | 23 | it("should provide a router", () => { 24 | render( 25 | 26 | 27 | 28 | ); 29 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 30 | }); 31 | 32 | it("using the singleton router should update the URL", () => { 33 | render( 34 | 35 | 36 | 37 | ); 38 | 39 | // Navigate: 40 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 41 | act(() => { 42 | singletonRouter.push("/new-route"); 43 | }); 44 | expect(screen.getByText(`Current route: "/new-route"`)).toBeDefined(); 45 | }); 46 | 47 | it("clicking a link should navigate to the new URL", () => { 48 | render( 49 | 50 | 51 | 52 | ); 53 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 54 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 55 | expect(screen.getByText(`Current route: "/test"`)).toBeDefined(); 56 | }); 57 | 58 | describe("url", () => { 59 | it("an initial URL can be supplied", () => { 60 | render( 61 | 62 | 63 | 64 | ); 65 | expect(screen.getByText(`Current route: "/example"`)).toBeDefined(); 66 | }); 67 | 68 | it("an initial URL Object can be supplied", () => { 69 | render( 70 | 71 | 72 | 73 | ); 74 | expect(screen.getByText(`Current route: "/example?foo=bar"`)).toBeDefined(); 75 | }); 76 | }); 77 | 78 | describe("async", () => { 79 | it("clicking a link should navigate to the new URL, asynchronously", async () => { 80 | render( 81 | 82 | 83 | 84 | ); 85 | expect(screen.getByText(`Current route: "/initial"`)).toBeDefined(); 86 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 87 | expect(screen.queryByText(`Current route: "/test"`)).toBeNull(); 88 | await waitFor(() => expect(screen.queryByText(`Current route: "/test"`)).not.toBeNull()); 89 | }); 90 | }); 91 | 92 | describe("events", () => { 93 | const eventHandlers = { 94 | onPush: jest.fn(), 95 | onReplace: jest.fn(), 96 | onRouteChangeStart: jest.fn(), 97 | onRouteChangeComplete: jest.fn(), 98 | }; 99 | beforeEach(() => { 100 | jest.clearAllMocks(); 101 | }); 102 | it("clicking a link should trigger the correct event handlers", () => { 103 | render( 104 | 105 | 106 | 107 | ); 108 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 109 | expect(screen.getByText(`Current route: "/test"`)).not.toBeNull(); 110 | expect(eventHandlers.onPush).toHaveBeenCalledWith("/test", { shallow: false }); 111 | expect(eventHandlers.onReplace).not.toHaveBeenCalled(); 112 | expect(eventHandlers.onRouteChangeStart).toHaveBeenCalledWith("/test", { shallow: false }); 113 | expect(eventHandlers.onRouteChangeComplete).toHaveBeenCalledWith("/test", { shallow: false }); 114 | }); 115 | it("a 'replace' link triggers the correct handlers too", () => { 116 | render( 117 | 118 | 119 | 120 | ); 121 | fireEvent.click(screen.getByText(`Current route: "/initial"`)); 122 | expect(screen.getByText(`Current route: "/test"`)).not.toBeNull(); 123 | expect(eventHandlers.onPush).not.toHaveBeenCalledWith("/test", { shallow: false }); 124 | expect(eventHandlers.onReplace).toHaveBeenCalled(); 125 | expect(eventHandlers.onRouteChangeStart).toHaveBeenCalledWith("/test", { shallow: false }); 126 | expect(eventHandlers.onRouteChangeComplete).toHaveBeenCalledWith("/test", { shallow: false }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/MemoryRouterProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode, useMemo } from "react"; 2 | 3 | import { useMemoryRouter, MemoryRouter, Url, default as singletonRouter } from "../index"; 4 | import { default as asyncSingletonRouter } from "../async"; 5 | import { MemoryRouterEventHandlers } from "../useMemoryRouter"; 6 | import { MemoryRouterContext } from "../MemoryRouterContext"; 7 | 8 | type AbstractedNextDependencies = Pick< 9 | typeof import("next/dist/shared/lib/router-context.shared-runtime"), 10 | "RouterContext" 11 | >; 12 | 13 | export type MemoryRouterProviderProps = { 14 | /** 15 | * The initial URL to render. 16 | */ 17 | url?: Url; 18 | async?: boolean; 19 | children?: ReactNode; 20 | } & MemoryRouterEventHandlers; 21 | 22 | export function factory(dependencies: AbstractedNextDependencies) { 23 | const { RouterContext } = dependencies; 24 | 25 | const MemoryRouterProvider: FC = ({ children, url, async, ...eventHandlers }) => { 26 | const memoryRouter = useMemo(() => { 27 | if (typeof url !== "undefined") { 28 | // If the `url` was specified, we'll use an "isolated router" instead of the singleton. 29 | return new MemoryRouter(url, async); 30 | } 31 | // Normally we'll just use the singleton: 32 | return async ? asyncSingletonRouter : singletonRouter; 33 | }, [url, async]); 34 | 35 | const routerSnapshot = useMemoryRouter(memoryRouter, eventHandlers); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | return MemoryRouterProvider; 45 | } 46 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/index.ts: -------------------------------------------------------------------------------- 1 | export declare const MemoryRouterProvider: typeof import("./next-13.5").MemoryRouterProvider; 2 | // Automatically try to export the correct version: 3 | try { 4 | module.exports = require("./next-13.5"); 5 | } catch (firstErr) { 6 | try { 7 | module.exports = require("./next-13"); 8 | } catch { 9 | try { 10 | module.exports = require("./next-12"); 11 | } catch { 12 | try { 13 | module.exports = require("./next-11"); 14 | } catch { 15 | try { 16 | module.exports = require("./next-10"); 17 | } catch { 18 | throw firstErr; 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/next-10/index.tsx: -------------------------------------------------------------------------------- 1 | // NOTE: this path works with Next v10-v11.0.1 2 | // @ts-expect-error 3 | import { RouterContext } from "next/dist/next-server/lib/router-context"; 4 | import { factory } from "../MemoryRouterProvider"; 5 | 6 | export const MemoryRouterProvider = factory({ RouterContext }); 7 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/next-11/index.tsx: -------------------------------------------------------------------------------- 1 | // NOTE: this path works with Next v11.1.0 through v13.4.0+ 2 | // @ts-ignore 3 | import { RouterContext } from "next/dist/shared/lib/router-context"; 4 | import { factory } from "../MemoryRouterProvider"; 5 | 6 | export const MemoryRouterProvider = factory({ RouterContext }); 7 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/next-12/index.tsx: -------------------------------------------------------------------------------- 1 | // No difference from Next 11: 2 | export * from "../next-11"; 3 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/next-13.5/index.tsx: -------------------------------------------------------------------------------- 1 | // NOTE: this path works with Next v13.5.0+ 2 | // 3 | import { RouterContext } from "next/dist/shared/lib/router-context.shared-runtime"; 4 | import { factory } from "../MemoryRouterProvider"; 5 | 6 | export const MemoryRouterProvider = factory({ RouterContext }); 7 | -------------------------------------------------------------------------------- /src/MemoryRouterProvider/next-13/index.tsx: -------------------------------------------------------------------------------- 1 | // No difference from Next 11: 2 | export * from "../next-11"; 3 | -------------------------------------------------------------------------------- /src/async/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { useRouterTests } from "../useMemoryRouter.test"; 2 | 3 | import router, { MemoryRouter, useRouter, withRouter } from "./index"; 4 | 5 | describe("next-overridable-hook/async", () => { 6 | it("should export a default router", () => { 7 | expect(router).toBeInstanceOf(MemoryRouter); 8 | expect(useRouter).toBeInstanceOf(Function); 9 | expect(withRouter).toBeInstanceOf(Function); 10 | }); 11 | 12 | describe("useRouter", () => { 13 | useRouterTests(router, useRouter); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/async/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { NextComponentType, NextPageContext } from "next"; 3 | import type { BaseContext } from "next/dist/shared/lib/utils"; 4 | import { MemoryRouter } from "../MemoryRouter"; 5 | import { useMemoryRouter } from "../useMemoryRouter"; 6 | import { withMemoryRouter, WithRouterProps } from "../withMemoryRouter"; 7 | import { MemoryRouterContext } from "../MemoryRouterContext"; 8 | 9 | // Export extra mock APIs: 10 | export { useMemoryRouter } from "../useMemoryRouter"; 11 | export { MemoryRouter, BaseRouter, Url } from "../MemoryRouter"; 12 | 13 | // Export the singleton: 14 | export const memoryRouter = new MemoryRouter(); 15 | memoryRouter.async = true; 16 | export default memoryRouter; 17 | 18 | // Export the `useRouter` hook: 19 | export const useRouter = () => { 20 | return ( 21 | React.useContext(MemoryRouterContext) || // Allow to override the singleton, if needed 22 | useMemoryRouter(memoryRouter) 23 | ); 24 | }; 25 | 26 | // Export the `withRouter` HOC: 27 | export const withRouter =

( 28 | ComposedComponent: NextComponentType 29 | ) => { 30 | return withMemoryRouter(useRouter, ComposedComponent); 31 | }; 32 | -------------------------------------------------------------------------------- /src/dynamic-routes/createDynamicRouteParser.test.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter } from "../MemoryRouter"; 2 | import { createDynamicRouteParser } from "./next-12"; 3 | import { expectMatch } from "../../test/test-utils"; 4 | 5 | describe("dynamic routes", () => { 6 | let memoryRouter: MemoryRouter; 7 | beforeEach(() => { 8 | memoryRouter = new MemoryRouter(); 9 | }); 10 | 11 | it("when dynamic path registered will parse variables from slug", () => { 12 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]/attribute/[name]", "/[...slug]"])); 13 | 14 | memoryRouter.push("/entity/101/attribute/everything"); 15 | expectMatch(memoryRouter, { 16 | pathname: "/entity/[id]/attribute/[name]", 17 | asPath: "/entity/101/attribute/everything", 18 | query: { 19 | id: "101", 20 | name: "everything", 21 | }, 22 | }); 23 | }); 24 | 25 | it("when catch-all dynamic path registered will parse variables from slug", () => { 26 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]/attribute/[name]", "/[...slug]"])); 27 | 28 | memoryRouter.push("/one/two/three"); 29 | expectMatch(memoryRouter, { 30 | pathname: "/[...slug]", 31 | asPath: "/one/two/three", 32 | query: { 33 | slug: ["one", "two", "three"], 34 | }, 35 | }); 36 | }); 37 | 38 | it("when no dynamic path matches, will not parse query from slug", () => { 39 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]/attribute/[name]"])); 40 | 41 | memoryRouter.push("/one/two/three"); 42 | expectMatch(memoryRouter, { 43 | pathname: "/one/two/three", 44 | asPath: "/one/two/three", 45 | query: {}, 46 | }); 47 | }); 48 | 49 | it("when both dynamic and static path matches, will use static path", () => { 50 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]", "/entity/list"])); 51 | 52 | memoryRouter.push("/entity/list"); 53 | expectMatch(memoryRouter, { 54 | pathname: "/entity/list", 55 | asPath: "/entity/list", 56 | query: {}, 57 | }); 58 | }); 59 | 60 | it("when query param matches path param, path param will take precedence", () => { 61 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]"])); 62 | 63 | memoryRouter.push("/entity/100?id=500"); 64 | 65 | expectMatch(memoryRouter, { 66 | asPath: "/entity/100?id=500", 67 | pathname: "/entity/[id]", 68 | query: { id: "100" }, 69 | }); 70 | }); 71 | 72 | it("when slug passed in pathname, pathname should be set to route and asPath interpolated from query", () => { 73 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]"])); 74 | 75 | memoryRouter.push({ pathname: "/entity/[id]", query: { id: "42" } }); 76 | 77 | expectMatch(memoryRouter, { 78 | pathname: "/entity/[id]", 79 | asPath: "/entity/42", 80 | query: { id: "42" }, 81 | }); 82 | }); 83 | 84 | it("when slug passed in pathname with additional query params, asPath should have query string", () => { 85 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]"])); 86 | 87 | memoryRouter.push({ pathname: "/entity/[id]", query: { id: "42", filter: "abc" } }); 88 | 89 | expectMatch(memoryRouter, { 90 | pathname: "/entity/[id]", 91 | asPath: "/entity/42?filter=abc", 92 | query: { id: "42", filter: "abc" }, 93 | }); 94 | }); 95 | 96 | it("will properly interpolate catch-all routes from the pathname", () => { 97 | memoryRouter.useParser(createDynamicRouteParser(["/[...slug]"])); 98 | 99 | memoryRouter.push({ pathname: "/[...slug]", query: { slug: ["one", "two", "three"] } }); 100 | 101 | expectMatch(memoryRouter, { 102 | pathname: "/[...slug]", 103 | asPath: "/one/two/three", 104 | query: { slug: ["one", "two", "three"] }, 105 | }); 106 | }); 107 | 108 | it("with dynamic routes, will properly generate asPath when passed in query dictionary", () => { 109 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]"])); 110 | 111 | memoryRouter.push({ pathname: "/entity/100", query: { filter: "abc", max: "1000" } }); 112 | 113 | expectMatch(memoryRouter, { 114 | pathname: "/entity/[id]", 115 | asPath: "/entity/100?filter=abc&max=1000", 116 | query: { id: "100", filter: "abc", max: "1000" }, 117 | }); 118 | }); 119 | 120 | it("will properly interpolate optional catch-all routes from the pathname", () => { 121 | memoryRouter.useParser(createDynamicRouteParser(["/one/two/[[...slug]]"])); 122 | 123 | memoryRouter.push("/one/two/three/four"); 124 | 125 | expectMatch(memoryRouter, { 126 | pathname: "/one/two/[[...slug]]", 127 | asPath: "/one/two/three/four", 128 | query: { slug: ["three", "four"] }, 129 | }); 130 | }); 131 | 132 | it("will match route with optional catch-all omitted", () => { 133 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]/[[...slug]]"])); 134 | 135 | memoryRouter.push("/entity/42"); 136 | 137 | expectMatch(memoryRouter, { 138 | pathname: "/entity/[id]/[[...slug]]", 139 | asPath: "/entity/42", 140 | query: { id: "42" }, 141 | }); 142 | }); 143 | 144 | describe('the "as" parameter', () => { 145 | beforeEach(() => { 146 | memoryRouter.useParser(createDynamicRouteParser(["/path/[testParam]"])); 147 | }); 148 | it('uses "as" path param with a dynamic route', async () => { 149 | memoryRouter.push("/path/[testParam]", "/path/456"); 150 | expectMatch(memoryRouter, { 151 | asPath: "/path/456", 152 | pathname: "/path/[testParam]", 153 | query: { 154 | testParam: "456", 155 | }, 156 | }); 157 | }); 158 | it('uses "as" path param over "url" path param', async () => { 159 | // This actually doesn't work well in Next, it forces a page refresh 160 | memoryRouter.push("/path/123", "/path/456"); 161 | expectMatch(memoryRouter, { 162 | asPath: "/path/456", 163 | pathname: "/path/[testParam]", 164 | query: { 165 | testParam: "456", 166 | }, 167 | }); 168 | }); 169 | it("merges the real query params with the route params", () => { 170 | memoryRouter.push( 171 | { 172 | pathname: "/path/[testParam]", 173 | query: { param: "href" }, 174 | }, 175 | "/path/456" 176 | ); 177 | expectMatch(memoryRouter, { 178 | asPath: "/path/456", 179 | pathname: "/path/[testParam]", 180 | query: { 181 | param: "href", 182 | testParam: "456", 183 | }, 184 | }); 185 | }); 186 | }); 187 | 188 | it("hashes are preserved", async () => { 189 | memoryRouter.useParser(createDynamicRouteParser(["/entity/[id]"])); 190 | 191 | memoryRouter.setCurrentUrl("/entity/42#hash"); 192 | expectMatch(memoryRouter, { 193 | asPath: "/entity/42#hash", 194 | pathname: "/entity/[id]", 195 | hash: "#hash", 196 | }); 197 | 198 | memoryRouter.setCurrentUrl("/entity/42?key=value#hash"); 199 | expectMatch(memoryRouter, { 200 | asPath: "/entity/42?key=value#hash", 201 | pathname: "/entity/[id]", 202 | query: { key: "value", id: "42" }, 203 | hash: "#hash", 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/dynamic-routes/createDynamicRouteParser.tsx: -------------------------------------------------------------------------------- 1 | import type { UrlObjectComplete } from "../MemoryRouter"; 2 | 3 | type AbstractedNextDependencies = Pick< 4 | typeof import("next/dist/shared/lib/router/utils") & 5 | typeof import("next/dist/shared/lib/page-path/normalize-page-path") & 6 | typeof import("next/dist/shared/lib/router/utils/route-matcher") & 7 | typeof import("next/dist/shared/lib/router/utils/route-regex"), 8 | "getSortedRoutes" | "getRouteMatcher" | "getRouteRegex" | "isDynamicRoute" | "normalizePagePath" 9 | >; 10 | 11 | /** 12 | * The only differences between Next 10/11/12/13 is the import paths, 13 | * so this "factory" function allows us to abstract these dependencies. 14 | */ 15 | export function factory(dependencies: AbstractedNextDependencies) { 16 | checkDependencies(dependencies); 17 | const { 18 | // 19 | getSortedRoutes, 20 | getRouteMatcher, 21 | getRouteRegex, 22 | isDynamicRoute, 23 | normalizePagePath, 24 | } = dependencies; 25 | 26 | return function createDynamicRouteParser(paths: string[]) { 27 | const matchers = getSortedRoutes(paths.map((path) => normalizePagePath(path))).map((path: string) => ({ 28 | pathname: path, 29 | match: getRouteMatcher(getRouteRegex(path)), 30 | })); 31 | 32 | return function parser(url: UrlObjectComplete): void { 33 | const pathname = url.pathname; 34 | const isDynamic = isDynamicRoute(pathname); 35 | const matcher = matchers.find((matcher) => matcher.match(pathname)); 36 | 37 | if (matcher) { 38 | // Update the route name: 39 | url.pathname = matcher.pathname; 40 | 41 | if (!isDynamic) { 42 | // Extract the route variables from the path: 43 | url.routeParams = matcher.match(pathname) || {}; 44 | } 45 | } 46 | }; 47 | }; 48 | } 49 | 50 | /** 51 | * Check that all these dependencies are properly defined 52 | */ 53 | function checkDependencies(dependencies: Record) { 54 | const missingDependencies = Object.keys(dependencies).filter((name) => { 55 | return !dependencies[name]; 56 | }); 57 | if (missingDependencies.length) { 58 | throw new Error( 59 | `next-router-mock/dynamic-routes: the following dependencies are missing: ${JSON.stringify(missingDependencies)}` 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/dynamic-routes/index.ts: -------------------------------------------------------------------------------- 1 | export declare const createDynamicRouteParser: typeof import("./next-13").createDynamicRouteParser; 2 | // Automatically try to export the correct version: 3 | try { 4 | module.exports = require("./next-13"); 5 | } catch (firstErr) { 6 | try { 7 | module.exports = require("./next-12"); 8 | } catch { 9 | try { 10 | module.exports = require("./next-11"); 11 | } catch { 12 | try { 13 | module.exports = require("./next-10"); 14 | } catch { 15 | throw firstErr; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/dynamic-routes/next-10/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getRouteMatcher, 3 | getRouteRegex, 4 | getSortedRoutes, 5 | isDynamicRoute, 6 | // @ts-expect-error 7 | } from "next/dist/next-server/lib/router/utils"; 8 | // @ts-expect-error 9 | import { normalizePagePath } from "next/dist/next-server/server/normalize-page-path"; 10 | 11 | import { factory } from "../createDynamicRouteParser"; 12 | 13 | export const createDynamicRouteParser = factory({ 14 | getSortedRoutes, 15 | getRouteMatcher, 16 | getRouteRegex, 17 | isDynamicRoute, 18 | normalizePagePath, 19 | }); 20 | -------------------------------------------------------------------------------- /src/dynamic-routes/next-11/index.ts: -------------------------------------------------------------------------------- 1 | import { getRouteMatcher } from "next/dist/shared/lib/router/utils/route-matcher"; 2 | import { getRouteRegex } from "next/dist/shared/lib/router/utils/route-regex"; 3 | import { 4 | getSortedRoutes, 5 | isDynamicRoute, 6 | // 7 | } from "next/dist/shared/lib/router/utils"; 8 | // @ts-expect-error 9 | import { normalizePagePath } from "next/dist/server/normalize-page-path"; 10 | 11 | import { factory } from "../createDynamicRouteParser"; 12 | 13 | export const createDynamicRouteParser = factory({ 14 | getSortedRoutes, 15 | getRouteMatcher, 16 | getRouteRegex, 17 | isDynamicRoute, 18 | normalizePagePath, 19 | }); 20 | -------------------------------------------------------------------------------- /src/dynamic-routes/next-12/index.ts: -------------------------------------------------------------------------------- 1 | import { getRouteMatcher } from "next/dist/shared/lib/router/utils/route-matcher"; 2 | import { getRouteRegex } from "next/dist/shared/lib/router/utils/route-regex"; 3 | import { 4 | getSortedRoutes, 5 | isDynamicRoute, 6 | // 7 | } from "next/dist/shared/lib/router/utils"; 8 | // 9 | import { normalizePagePath } from "next/dist/shared/lib/page-path/normalize-page-path"; 10 | 11 | import { factory } from "../createDynamicRouteParser"; 12 | 13 | export const createDynamicRouteParser = factory({ 14 | getSortedRoutes, 15 | getRouteMatcher, 16 | getRouteRegex, 17 | isDynamicRoute, 18 | normalizePagePath, 19 | }); 20 | -------------------------------------------------------------------------------- /src/dynamic-routes/next-13/index.ts: -------------------------------------------------------------------------------- 1 | // No difference from Next 12: 2 | export * from "../next-12"; 3 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { useRouterTests } from "./useMemoryRouter.test"; 2 | 3 | import router, { MemoryRouter, useRouter, withRouter } from "./index"; 4 | 5 | describe("next-overridable-hook", () => { 6 | it("should export a default router", () => { 7 | expect(router).toBeInstanceOf(MemoryRouter); 8 | expect(useRouter).toBeInstanceOf(Function); 9 | expect(withRouter).toBeInstanceOf(Function); 10 | }); 11 | 12 | it("the router should have several default properties set", () => { 13 | expect(router).toEqual({ 14 | // Ignore these: 15 | events: expect.any(Object), 16 | internal: expect.any(Object), 17 | async: expect.any(Boolean), 18 | push: expect.any(Function), 19 | replace: expect.any(Function), 20 | setCurrentUrl: expect.any(Function), 21 | // Ensure the router has exactly these properties: 22 | asPath: "/", 23 | basePath: "", 24 | hash: "", 25 | isFallback: false, 26 | isLocaleDomain: false, 27 | isPreview: false, 28 | isReady: true, 29 | locale: undefined, 30 | locales: [], 31 | pathname: "/", 32 | query: {}, 33 | }); 34 | }); 35 | 36 | describe("useRouter", () => { 37 | useRouterTests(router, useRouter); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { NextComponentType, NextPageContext } from "next"; 3 | import type { BaseContext } from "next/dist/shared/lib/utils"; 4 | import { MemoryRouter } from "./MemoryRouter"; 5 | import { useMemoryRouter } from "./useMemoryRouter"; 6 | import { withMemoryRouter, WithRouterProps } from "./withMemoryRouter"; 7 | import { MemoryRouterContext } from "./MemoryRouterContext"; 8 | 9 | // Export extra mock APIs: 10 | export { useMemoryRouter } from "./useMemoryRouter"; 11 | export { MemoryRouter, BaseRouter, Url } from "./MemoryRouter"; 12 | 13 | // Export the singleton: 14 | export const memoryRouter = new MemoryRouter(); 15 | memoryRouter.async = false; 16 | export default memoryRouter; 17 | 18 | // Export the `useRouter` hook: 19 | export const useRouter = () => { 20 | return ( 21 | React.useContext(MemoryRouterContext) || // Allow to override the singleton, if needed 22 | useMemoryRouter(memoryRouter) 23 | ); 24 | }; 25 | 26 | // Export the `withRouter` HOC: 27 | export const withRouter =

( 28 | ComposedComponent: NextComponentType 29 | ) => { 30 | return withMemoryRouter(useRouter, ComposedComponent); 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/mitt/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | Copyright (c) Jason Miller (https://jasonformat.com/) 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | */ 8 | 9 | // This file is based on https://github.com/developit/mitt/blob/v1.1.3/src/index.js 10 | // It's been edited for the needs of this script 11 | // See the LICENSE at the top of the file 12 | 13 | type Handler = (...evts: any[]) => void; 14 | 15 | export type MittEmitter = { 16 | on(type: T, handler: Handler): void; 17 | off(type: T, handler: Handler): void; 18 | emit(type: T, ...evts: any[]): void; 19 | }; 20 | 21 | export default function mitt(): MittEmitter { 22 | const all: { [s: string]: Handler[] } = Object.create(null); 23 | 24 | return { 25 | on(type: string, handler: Handler) { 26 | (all[type] || (all[type] = [])).push(handler); 27 | }, 28 | 29 | off(type: string, handler: Handler) { 30 | if (all[type]) { 31 | all[type].splice(all[type].indexOf(handler) >>> 0, 1); 32 | } 33 | }, 34 | 35 | emit(type: string, ...evts: any[]) { 36 | // eslint-disable-next-line array-callback-return 37 | (all[type] || []).slice().map((handler: Handler) => { 38 | handler(...evts); 39 | }); 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/navigation/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, renderHook, RenderHookResult } from "@testing-library/react"; 2 | import singletonRouter from "../index"; 3 | import { 4 | useRouter, 5 | useParams, 6 | usePathname, 7 | useSearchParams, 8 | useSelectedLayoutSegment, 9 | useSelectedLayoutSegments, 10 | } from "./index"; 11 | import { createDynamicRouteParser } from "../dynamic-routes"; 12 | 13 | describe("next/navigation", () => { 14 | beforeEach(() => { 15 | singletonRouter.reset(); 16 | }); 17 | 18 | describe("useRouter", () => { 19 | const hook = beforeEachRenderHook(() => useRouter()); 20 | 21 | it("should be a snapshot of the router", () => { 22 | expect(hook.result.current).not.toBe(singletonRouter); 23 | expect(hook.result.current).toMatchObject({ 24 | push: expect.any(Function), 25 | replace: expect.any(Function), 26 | refresh: expect.any(Function), 27 | prefetch: expect.any(Function), 28 | back: expect.any(Function), 29 | forward: expect.any(Function), 30 | }); 31 | }); 32 | it("returns the same object after rerendering", () => { 33 | const initial = hook.result.current; 34 | hook.rerender(); 35 | expect(hook.result.current).toBe(initial); 36 | }); 37 | it("pushing a route does not trigger a rerender", () => { 38 | const initial = hook.result.current; 39 | act(() => { 40 | initial.push("/url"); 41 | }); 42 | expect(hook.result.current).toBe(initial); 43 | }); 44 | }); 45 | 46 | describe("usePathname", () => { 47 | const hook = beforeEachRenderHook(() => usePathname()); 48 | it("should return the current pathname", () => { 49 | expect(hook.result.current).toEqual("/"); 50 | }); 51 | it("should update when a new path is pushed", () => { 52 | act(() => { 53 | singletonRouter.push("/new-path"); 54 | }); 55 | 56 | expect(hook.result.current).toEqual("/new-path"); 57 | }); 58 | }); 59 | 60 | describe("useParams", () => { 61 | beforeEach(() => { 62 | singletonRouter.useParser(createDynamicRouteParser(["/[one]/[two]"])); 63 | }); 64 | const hook = beforeEachRenderHook(() => useParams()); 65 | it("should contain the route params", () => { 66 | expect(hook.result.current).toEqual({}); 67 | 68 | act(() => { 69 | singletonRouter.push("/A/B"); 70 | }); 71 | 72 | expect(hook.result.current).toEqual({ one: "A", two: "B" }); 73 | }); 74 | 75 | it("should not contain search params", () => { 76 | expect(hook.result.current).toEqual({}); 77 | 78 | act(() => { 79 | singletonRouter.push("/A/B?one=ONE&two=TWO&three=THREE"); 80 | }); 81 | 82 | expect(hook.result.current).toEqual({ one: "A", two: "B" }); 83 | }); 84 | }); 85 | 86 | describe("useSearchParams", () => { 87 | const hook = beforeEachRenderHook(() => useSearchParams()); 88 | it("should contain the search params", () => { 89 | expect([...hook.result.current.entries()]).toEqual([]); 90 | 91 | act(() => { 92 | singletonRouter.push("/path?one=1&two=2&three=3"); 93 | }); 94 | 95 | expect([...hook.result.current.entries()]).toEqual([ 96 | ["one", "1"], 97 | ["two", "2"], 98 | ["three", "3"], 99 | ]); 100 | }); 101 | }); 102 | 103 | describe("useSelectedLayoutSegment", () => { 104 | act(() => { 105 | singletonRouter.push("/segment1/segment2"); 106 | }); 107 | const hook = beforeEachRenderHook(() => useSelectedLayoutSegment()); 108 | it("should show not implemented yet", () => { 109 | expect(hook.result.current).toEqual("[next-router-mock] Not Yet Implemented"); 110 | }); 111 | }); 112 | 113 | describe("useSelectedLayoutSegments", () => { 114 | act(() => { 115 | singletonRouter.push("/segment1/segment2"); 116 | }); 117 | const hook = beforeEachRenderHook(() => useSelectedLayoutSegments()); 118 | it("should show not implemented yet", () => { 119 | expect(hook.result.current).toEqual(["[next-router-mock] Not Yet Implemented"]); 120 | }); 121 | }); 122 | }); 123 | 124 | function beforeEachRenderHook(render: () => T): RenderHookResult { 125 | const hookResult = {} as any; 126 | beforeEach(() => { 127 | const newHookResult = renderHook(render); 128 | Object.assign(hookResult, newHookResult); 129 | }); 130 | return hookResult; 131 | } 132 | -------------------------------------------------------------------------------- /src/navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | import { MemoryRouter } from "../MemoryRouter"; 3 | import singletonRouter from "../index"; 4 | import type * as NextNav from "next/navigation"; 5 | 6 | function useSnapshot(makeSnapshot: (r: MemoryRouter, prev: T | null) => T): T { 7 | const [snapshot, setSnapshot] = useState(() => makeSnapshot(singletonRouter, null)); 8 | 9 | useEffect(() => { 10 | // To ensure we don't call setRouter after unmounting: 11 | let isMounted = true; 12 | 13 | const handleRouteChange = () => { 14 | if (!isMounted) return; 15 | 16 | // Ensure the reference changes each render: 17 | setSnapshot((prev) => makeSnapshot(singletonRouter, prev)); 18 | }; 19 | 20 | singletonRouter.events.on("routeChangeComplete", handleRouteChange); 21 | singletonRouter.events.on("hashChangeComplete", handleRouteChange); 22 | return () => { 23 | isMounted = false; 24 | singletonRouter.events.off("routeChangeComplete", handleRouteChange); 25 | singletonRouter.events.off("hashChangeComplete", handleRouteChange); 26 | }; 27 | }, []); 28 | 29 | return snapshot; 30 | } 31 | 32 | export const useRouter: typeof NextNav.useRouter = () => { 33 | // All these methods are static, and never trigger a rerender: 34 | return useMemo( 35 | () => ({ 36 | push: (url, options) => singletonRouter.push(url), 37 | replace: (url, options) => singletonRouter.replace(url), 38 | refresh: singletonRouter.reload, 39 | prefetch: singletonRouter.prefetch, 40 | back: singletonRouter.back, 41 | forward: singletonRouter.forward, 42 | }), 43 | [] 44 | ); 45 | }; 46 | 47 | export const useSearchParams: typeof NextNav.useSearchParams = () => { 48 | return useSnapshot((r, prev) => { 49 | const query = r.internal.query; 50 | // Build the search params from the query object: 51 | const newSearchParams = new URLSearchParams(); 52 | Object.keys(query).forEach((key) => { 53 | const value = query[key]; 54 | if (Array.isArray(value)) { 55 | value.forEach((val) => newSearchParams.append(key, val)); 56 | } else if (value !== undefined) { 57 | newSearchParams.append(key, value); 58 | } 59 | }); 60 | 61 | // Prevent rerendering if the query is the same: 62 | if (prev && newSearchParams.toString() === prev.toString()) { 63 | return prev; 64 | } 65 | return newSearchParams as NextNav.ReadonlyURLSearchParams; 66 | }); 67 | }; 68 | 69 | export const usePathname: typeof NextNav.usePathname = () => { 70 | return useSnapshot((r) => r.pathname); 71 | }; 72 | 73 | export const useParams: typeof NextNav.useParams = >() => { 74 | return useSnapshot((r) => r.internal.routeParams as T); 75 | }; 76 | 77 | export const useSelectedLayoutSegment: typeof NextNav.useSelectedLayoutSegment = () => 78 | useSnapshot((r) => r.internal.selectedLayoutSegment); 79 | export const useSelectedLayoutSegments: typeof NextNav.useSelectedLayoutSegments = () => 80 | useSnapshot((r) => r.internal.selectedLayoutSegments); 81 | -------------------------------------------------------------------------------- /src/next-link.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "next/link"; 3 | import { fireEvent, render, screen, waitFor } from "@testing-library/react"; 4 | 5 | import memoryRouter from "./index"; 6 | import { MemoryRouterProvider } from "./MemoryRouterProvider"; 7 | 8 | jest.mock("next/dist/client/router", () => require("./index")); 9 | 10 | const wrapper = (props: { children: React.ReactNode }) => ; 11 | 12 | describe("next/link", () => { 13 | describe("clicking a link will mock navigate", () => { 14 | it("to a href", async () => { 15 | render(Example Link, { wrapper }); 16 | fireEvent.click(screen.getByText("Example Link")); 17 | await waitFor(() => { 18 | expect(memoryRouter).toMatchObject({ 19 | asPath: "/example?foo=bar", 20 | pathname: "/example", 21 | query: { foo: "bar" }, 22 | }); 23 | }); 24 | }); 25 | 26 | it("to a URL object", async () => { 27 | render(Example Link, { wrapper }); 28 | fireEvent.click(screen.getByText("Example Link")); 29 | await waitFor(() => { 30 | expect(memoryRouter).toMatchObject({ 31 | asPath: "/example?foo=bar", 32 | pathname: "/example", 33 | query: { foo: "bar" }, 34 | }); 35 | }); 36 | }); 37 | 38 | it("supports multivalued query properties", async () => { 39 | render(Next Link, { 40 | wrapper, 41 | }); 42 | fireEvent.click(screen.getByText("Next Link")); 43 | await waitFor(() => { 44 | expect(memoryRouter).toMatchObject({ 45 | asPath: "/example?foo=bar&foo=baz", 46 | pathname: "/example", 47 | query: { foo: ["bar", "baz"] }, 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/urls.ts: -------------------------------------------------------------------------------- 1 | import type { NextRouter } from "next/router"; 2 | import type { UrlObject } from "./MemoryRouter"; 3 | 4 | export function parseUrl(url: string): UrlObject { 5 | const base = "https://base.com"; // base can be anything 6 | const parsed = new URL(url, base); 7 | const query = Object.fromEntries( 8 | Array.from(parsed.searchParams.keys()).map((key) => { 9 | const values = parsed.searchParams.getAll(key); 10 | return [key, values.length === 1 ? values[0] : values]; 11 | }) 12 | ); 13 | return { 14 | pathname: parsed.pathname, 15 | hash: parsed.hash, 16 | query, 17 | }; 18 | } 19 | export function stringifyQueryString(query: NextRouter["query"]): string { 20 | const params = new URLSearchParams(); 21 | Object.keys(query).forEach((key) => { 22 | const values = query[key]; 23 | for (const value of Array.isArray(values) ? values : [values]) { 24 | params.append(key, value!); 25 | } 26 | }); 27 | return params.toString(); 28 | } 29 | export function parseQueryString(query: string): NextRouter["query"] | undefined { 30 | const parsedUrl = parseUrl(`?${query}`); 31 | 32 | return parsedUrl.query; 33 | } 34 | -------------------------------------------------------------------------------- /src/useMemoryRouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { act, renderHook } from "@testing-library/react"; 3 | 4 | import { MemoryRouter, MemoryRouterSnapshot } from "./MemoryRouter"; 5 | import { useMemoryRouter } from "./useMemoryRouter"; 6 | 7 | export function useRouterTests(singletonRouter: MemoryRouter, useRouter: () => MemoryRouterSnapshot) { 8 | it("the useRouter hook only returns a snapshot of the singleton router", async () => { 9 | const { result } = renderHook(() => useRouter()); 10 | 11 | expect(result.current).not.toBe(singletonRouter); 12 | }); 13 | 14 | it("will allow capturing previous route values in hooks with routing events", async () => { 15 | // see: https://github.com/streamich/react-use/blob/master/src/usePrevious.ts 16 | const usePrevious = function (value: T): T | undefined { 17 | const previous = useRef(); 18 | 19 | useEffect(() => { 20 | previous.current = value; 21 | }); 22 | 23 | return previous.current; 24 | }; 25 | 26 | const useRouterWithPrevious = () => { 27 | const { asPath } = useRouter(); 28 | const previousAsPath = usePrevious(asPath); 29 | 30 | return [previousAsPath, asPath]; 31 | }; 32 | 33 | // Set initial state: 34 | singletonRouter.setCurrentUrl("/foo"); 35 | 36 | const { result } = renderHook(() => useRouterWithPrevious()); 37 | 38 | expect(result.current).toEqual([undefined, "/foo"]); 39 | 40 | await act(async () => { 41 | await singletonRouter.push("/foo?bar=baz"); 42 | }); 43 | 44 | expect(result.current).toEqual(["/foo", "/foo?bar=baz"]); 45 | }); 46 | 47 | it('"push" will cause a rerender with the new route', async () => { 48 | const { result } = renderHook(() => useRouter()); 49 | 50 | await act(async () => { 51 | await result.current.push("/foo?bar=baz"); 52 | }); 53 | 54 | expect(result.current).not.toBe(singletonRouter); 55 | expect(result.current).toEqual(singletonRouter); 56 | expect(result.current).toMatchObject({ 57 | asPath: "/foo?bar=baz", 58 | pathname: "/foo", 59 | query: { bar: "baz" }, 60 | }); 61 | }); 62 | 63 | it('changing just the "hash" will cause a rerender', async () => { 64 | const { result } = renderHook(() => useRouter()); 65 | 66 | await act(async () => { 67 | await result.current.push("/foo"); 68 | await result.current.push("/foo#bar"); 69 | }); 70 | const expected = { 71 | asPath: "/foo#bar", 72 | pathname: "/foo", 73 | hash: "#bar", 74 | }; 75 | expect(singletonRouter).toMatchObject(expected); 76 | expect(result.current).toMatchObject(expected); 77 | }); 78 | 79 | it('calling "push" multiple times will rerender with the correct route', async () => { 80 | const { result } = renderHook(() => useRouter()); 81 | 82 | // Push using the router instance: 83 | await act(async () => { 84 | result.current.push("/one"); 85 | result.current.push("/two"); 86 | await result.current.push("/three"); 87 | }); 88 | 89 | expect(result.current).toMatchObject({ 90 | asPath: "/three", 91 | }); 92 | 93 | // Push using the singleton router: 94 | await act(async () => { 95 | singletonRouter.push("/four"); 96 | singletonRouter.push("/five"); 97 | await singletonRouter.push("/six"); 98 | }); 99 | expect(result.current).toMatchObject({ 100 | asPath: "/six", 101 | }); 102 | 103 | // Push using the router instance (again): 104 | await act(async () => { 105 | result.current.push("/seven"); 106 | result.current.push("/eight"); 107 | await result.current.push("/nine"); 108 | }); 109 | 110 | expect(result.current).toMatchObject({ 111 | asPath: "/nine", 112 | }); 113 | }); 114 | 115 | it("the singleton and the router instances can be used interchangeably", async () => { 116 | const { result } = renderHook(() => useRouter()); 117 | await act(async () => { 118 | await result.current.push("/one"); 119 | }); 120 | expect(result.current).toMatchObject({ asPath: "/one" }); 121 | expect(result.current).toMatchObject(singletonRouter); 122 | 123 | await act(async () => { 124 | await result.current.push("/two"); 125 | }); 126 | expect(result.current).toMatchObject({ asPath: "/two" }); 127 | expect(result.current).toMatchObject(singletonRouter); 128 | 129 | await act(async () => { 130 | await singletonRouter.push("/three"); 131 | }); 132 | expect(result.current).toMatchObject({ asPath: "/three" }); 133 | expect(result.current).toMatchObject(singletonRouter); 134 | }); 135 | 136 | it("support the locales and locale properties", async () => { 137 | const { result } = renderHook(() => useRouter()); 138 | expect(result.current.locale).toBe(undefined); 139 | expect(result.current.locales).toEqual([]); 140 | 141 | await act(async () => { 142 | await result.current.push("/", undefined, { locale: "en" }); 143 | }); 144 | expect(result.current.locale).toBe("en"); 145 | }); 146 | } 147 | 148 | describe("useMemoryRouter", () => { 149 | const singletonRouter = new MemoryRouter(); 150 | const useRouter = () => useMemoryRouter(singletonRouter); 151 | useRouterTests(singletonRouter, useRouter); 152 | }); 153 | -------------------------------------------------------------------------------- /src/useMemoryRouter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { MemoryRouter } from "./MemoryRouter"; 4 | 5 | export type MemoryRouterEventHandlers = { 6 | onHashChangeStart?: (url: string, options: { shallow: boolean }) => void; 7 | onHashChangeComplete?: (url: string, options: { shallow: boolean }) => void; 8 | onRouteChangeStart?: (url: string, options: { shallow: boolean }) => void; 9 | onRouteChangeComplete?: (url: string, options: { shallow: boolean }) => void; 10 | onPush?: (url: string, options: { shallow: boolean }) => void; 11 | onReplace?: (url: string, options: { shallow: boolean }) => void; 12 | }; 13 | 14 | export const useMemoryRouter = (singletonRouter: MemoryRouter, eventHandlers?: MemoryRouterEventHandlers) => { 15 | const [router, setRouter] = useState(() => MemoryRouter.snapshot(singletonRouter)); 16 | 17 | // Trigger updates on route changes: 18 | useEffect(() => { 19 | // To ensure we don't call setRouter after unmounting: 20 | let isMounted = true; 21 | 22 | const handleRouteChange = () => { 23 | if (!isMounted) return; 24 | 25 | // Ensure the reference changes each render: 26 | setRouter(MemoryRouter.snapshot(singletonRouter)); 27 | }; 28 | 29 | singletonRouter.events.on("routeChangeComplete", handleRouteChange); 30 | singletonRouter.events.on("hashChangeComplete", handleRouteChange); 31 | return () => { 32 | isMounted = false; 33 | singletonRouter.events.off("routeChangeComplete", handleRouteChange); 34 | singletonRouter.events.off("hashChangeComplete", handleRouteChange); 35 | }; 36 | }, [singletonRouter]); 37 | 38 | // Subscribe to any eventHandlers: 39 | useEffect(() => { 40 | if (!eventHandlers) return; 41 | const { 42 | // 43 | onRouteChangeStart, 44 | onRouteChangeComplete, 45 | onHashChangeStart, 46 | onHashChangeComplete, 47 | onPush, 48 | onReplace, 49 | } = eventHandlers; 50 | if (onRouteChangeStart) singletonRouter.events.on("routeChangeStart", onRouteChangeStart); 51 | if (onRouteChangeComplete) singletonRouter.events.on("routeChangeComplete", onRouteChangeComplete); 52 | if (onHashChangeStart) singletonRouter.events.on("hashChangeStart", onHashChangeStart); 53 | if (onHashChangeComplete) singletonRouter.events.on("hashChangeComplete", onHashChangeComplete); 54 | if (onPush) singletonRouter.events.on("NEXT_ROUTER_MOCK:push", onPush); 55 | if (onReplace) singletonRouter.events.on("NEXT_ROUTER_MOCK:replace", onReplace); 56 | return () => { 57 | if (onRouteChangeStart) singletonRouter.events.off("routeChangeStart", onRouteChangeStart); 58 | if (onRouteChangeComplete) singletonRouter.events.off("routeChangeComplete", onRouteChangeComplete); 59 | if (onHashChangeStart) singletonRouter.events.off("hashChangeStart", onHashChangeStart); 60 | if (onHashChangeComplete) singletonRouter.events.off("hashChangeComplete", onHashChangeComplete); 61 | if (onPush) singletonRouter.events.off("NEXT_ROUTER_MOCK:push", onPush); 62 | if (onReplace) singletonRouter.events.off("NEXT_ROUTER_MOCK:replace", onReplace); 63 | }; 64 | }, [ 65 | singletonRouter.events, 66 | eventHandlers?.onRouteChangeStart, 67 | eventHandlers?.onRouteChangeComplete, 68 | eventHandlers?.onHashChangeStart, 69 | eventHandlers?.onHashChangeComplete, 70 | eventHandlers?.onPush, 71 | eventHandlers?.onReplace, 72 | ]); 73 | 74 | return router; 75 | }; 76 | -------------------------------------------------------------------------------- /src/withMemoryRouter.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { NextRouter } from "next/router"; 3 | import { act, render, screen } from "@testing-library/react"; 4 | import { memoryRouter, withRouter } from "./index"; 5 | 6 | class TestComponent extends Component<{ router: NextRouter; title?: string }, {}> { 7 | render() { 8 | return ( 9 | 10 | {this.props.title || "Current path"}: "{this.props.router.asPath}" 11 | 12 | ); 13 | } 14 | 15 | static getInitialProps() {} 16 | } 17 | 18 | const TestComponentWrapper = withRouter(TestComponent); 19 | 20 | describe("withRouter", () => { 21 | beforeEach(() => { 22 | memoryRouter.setCurrentUrl("/test"); 23 | }); 24 | 25 | it("should have access to the current router", async () => { 26 | render(); 27 | expect(screen.getByText('Current path: "/test"')).toBeDefined(); 28 | }); 29 | 30 | it("should respond to updates", () => { 31 | render(); 32 | act(() => { 33 | memoryRouter.push("/updated-path"); 34 | }); 35 | expect(screen.getByText('Current path: "/updated-path"')).toBeDefined(); 36 | }); 37 | 38 | it("should pass-through extra properties", () => { 39 | render(); 40 | expect(screen.getByText('CURRENT PATH: "/test"')).toBeDefined(); 41 | }); 42 | 43 | it("should copy the static `getInitialProps` method", () => { 44 | expect(TestComponentWrapper.getInitialProps).toBe(TestComponent.getInitialProps); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/withMemoryRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { NextComponentType, NextPageContext } from "next"; 3 | import type { BaseContext } from "next/dist/shared/lib/utils"; 4 | import type { NextRouter } from "next/router"; 5 | 6 | // This is a (very slightly) modified version of https://github.com/vercel/next.js/blob/canary/packages/next/client/with-router.tsx 7 | 8 | import type { MemoryRouterSnapshot } from "./MemoryRouter"; 9 | 10 | export type WithRouterProps = { 11 | router: NextRouter; 12 | }; 13 | 14 | export type ExcludeRouterProps

= Pick>; 15 | 16 | export function withMemoryRouter

( 17 | useRouter: () => MemoryRouterSnapshot, 18 | ComposedComponent: NextComponentType 19 | ): NextComponentType> { 20 | function WithRouterWrapper(props: any): JSX.Element { 21 | return ; 22 | } 23 | 24 | WithRouterWrapper.getInitialProps = ComposedComponent.getInitialProps; 25 | // This is needed to allow checking for custom getInitialProps in _app 26 | WithRouterWrapper.origGetInitialProps = (ComposedComponent as any).origGetInitialProps; 27 | if (process.env.NODE_ENV !== "production") { 28 | const name = ComposedComponent.displayName || ComposedComponent.name || "Unknown"; 29 | WithRouterWrapper.displayName = `withRouter(${name})`; 30 | } 31 | 32 | return WithRouterWrapper; 33 | } 34 | -------------------------------------------------------------------------------- /test/example-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /test/example-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /test/example-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /test/example-app/components/Search.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider/next-13"; 2 | import { Search } from "./Search"; 3 | 4 | const MemoryRouterDecorator = (Story: React.ComponentType) => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default { 11 | title: "components / Search", 12 | component: Search, 13 | decorators: [MemoryRouterDecorator], 14 | }; 15 | -------------------------------------------------------------------------------- /test/example-app/components/Search.test.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from "./Search"; 2 | 3 | jest.mock("next/router", () => jest.requireActual("next-router-mock")); 4 | 5 | describe("Search", () => { 6 | it("TODO add tests", () => { 7 | expect(typeof Search).toBe("function"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/example-app/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | /** 5 | * A Search input that keeps the `search` parameter in the current URL 6 | */ 7 | export const Search = () => { 8 | const router = useRouter(); 9 | 10 | const value = router.query.search || ""; 11 | 12 | const onChange = (ev: React.ChangeEvent) => { 13 | const newSearch = ev.currentTarget.value; 14 | router.replace({ 15 | query: { 16 | ...router.query, 17 | search: newSearch, 18 | }, 19 | }); 20 | }; 21 | 22 | return ; 23 | }; 24 | -------------------------------------------------------------------------------- /test/example-app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /test/example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "checks": "npm run test && npm run typecheck", 7 | "test": "echo TODO add tests", 8 | "typecheck": "tsc --noEmit", 9 | "dev": "next dev" 10 | }, 11 | "dependencies": { 12 | "next": "^13.1.2", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.20.12", 18 | "@storybook/addon-actions": "^6.5.15", 19 | "@types/node": "^18.11.9", 20 | "@types/react": "^18.0.26", 21 | "@types/react-dom": "^18.0.10", 22 | "babel-loader": "^8.3.0", 23 | "typescript": "^4.9.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/example-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /test/example-app/pages/as-tests/[[...route]].tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, PropsWithChildren, ReactEventHandler, useState } from "react"; 2 | import NextLink, { LinkProps as NextLinkProps } from "next/link"; 3 | import { Router, useRouter } from "next/router"; 4 | 5 | const root = "/as-tests"; 6 | 7 | const Page = () => { 8 | const [activeId, setActiveId] = useState(-1); 9 | const [expectedValues, setExpectedValues] = useState>({ 10 | asPath: "", 11 | pathname: "", 12 | query: {}, 13 | }); 14 | 15 | let index = 0; 16 | const expected = (expectedDetails: typeof expectedValues) => { 17 | const id = index++; 18 | return { 19 | onClick: () => { 20 | setActiveId(id); 21 | setExpectedValues(expectedDetails); 22 | }, 23 | active: id === activeId, 24 | }; 25 | }; 26 | 27 | const router = useRouter(); 28 | const values = { 29 | "root path": root, 30 | "router.asPath": router.asPath.replace(root, ""), 31 | "expect.asPath": expectedValues.asPath, 32 | "router.pathname": router.pathname.replace(root, ""), 33 | "expect.pathname": expectedValues.pathname, 34 | "router.query": router.query, 35 | "expect.query": expectedValues.query, 36 | }; 37 | const pathname = "/[[...route]]"; 38 | 39 | return ( 40 | <> 41 |

42 | Links with different query strings 43 | 44 | 54 | 55 | 65 | 66 | 76 | 77 | 87 |
88 | 89 |
90 | Dynamic Paths 91 | 92 | 102 | 103 | 113 | 114 | 124 | 125 | 135 |
136 | 137 |
138 | Links with different static paths 139 | 149 | 150 | 160 | 161 | 171 |
172 | 173 |
174 | Links with different dynamic paths (can cause full-page refresh) 175 | 185 | 186 | 196 |
197 | 198 |
199 | Router Details 200 | 201 |
202 | 203 | ); 204 | }; 205 | const TestLink: FC< 206 | Pick & { 207 | label: string; 208 | active: boolean; 209 | onClick: ReactEventHandler; 210 | } 211 | > = ({ label, href, as, active, onClick }) => { 212 | const normalizeUrl = (url: typeof as) => { 213 | // Prepend the 'root' URL: 214 | if (typeof url === "string") { 215 | return root + url; 216 | } 217 | if (typeof url === "object") { 218 | url = { ...url, pathname: root + (url.pathname || "") }; 219 | } 220 | return url; 221 | }; 222 | return ( 223 | 231 | {label} 232 | (href {JSON.stringify(href)} as {JSON.stringify(as)}) 233 | 234 | ); 235 | }; 236 | 237 | const DetailsTable: FC<{ values: object }> = ({ values }) => { 238 | return ( 239 | 240 | {Object.keys(values).map((key) => { 241 | const value = values[key as keyof typeof values]; 242 | return ( 243 | 244 | {JSON.stringify(value)} 245 | 246 | ); 247 | })} 248 |
249 | ); 250 | }; 251 | 252 | const Table: FC> = ({ children }) => { 253 | return ( 254 | 255 | {children} 256 |
257 | ); 258 | }; 259 | 260 | const Row: FC< 261 | PropsWithChildren<{ 262 | label: string; 263 | }> 264 | > = ({ label = "", children }) => { 265 | return ( 266 | 267 | 268 | {label} 269 | 270 | {children} 271 | 272 | ); 273 | }; 274 | 275 | export default Page; 276 | -------------------------------------------------------------------------------- /test/example-app/pages/as-tests/as-path.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "./[[...route]]"; 2 | -------------------------------------------------------------------------------- /test/example-app/pages/as-tests/real-path.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "./[[...route]]"; 2 | -------------------------------------------------------------------------------- /test/example-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottrippey/next-router-mock/aa891b21af1168e32bb6eb5a4279e86ccf1dcca9/test/example-app/public/favicon.ico -------------------------------------------------------------------------------- /test/example-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/example-app/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/example-app/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | html { 15 | color-scheme: dark; 16 | } 17 | body { 18 | color: white; 19 | background: black; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/example-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "next-router-mock": ["../.."], 19 | "next-router-mock/*": ["../../*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /test/next-10/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-10/next-10.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-10"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-10"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-10", () => { 15 | require("next-router-mock/dynamic-routes/next-10"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-10", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-10"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "10.2.3", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-11/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-11/next-11.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-11"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-11"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-11", () => { 15 | require("next-router-mock/dynamic-routes/next-11"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-11", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-11"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "11.1.4", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-12.2+/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-12.2+/next-12.2.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-12"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-12"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-12", () => { 15 | require("next-router-mock/dynamic-routes/next-12"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-12", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-12"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-12.2+/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-12.2+", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "next": "12.3.2", 9 | "next-router-mock": "../.." 10 | } 11 | }, 12 | "../..": { 13 | "version": "1.0.0", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@changesets/cli": "^2.26.2", 17 | "@testing-library/react": "^13.4.0", 18 | "@types/jest": "^26.0.20", 19 | "doctoc": "^2.2.0", 20 | "jest": "^26.6.3", 21 | "next": "^13.5.1", 22 | "prettier": "^2.2.1", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-test-renderer": "^18.2.0", 26 | "rimraf": "^3.0.2", 27 | "ts-jest": "^26.4.4", 28 | "typescript": "^4.9.5" 29 | }, 30 | "peerDependencies": { 31 | "next": ">=10.0.0", 32 | "react": ">=17.0.0" 33 | } 34 | }, 35 | "node_modules/@next/env": { 36 | "version": "12.3.2", 37 | "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.2.tgz", 38 | "integrity": "sha512-upwtMaHxlv/udAWGq0kE+rg8huwmcxQPsKZFhS1R5iVO323mvxEBe1YrSXe1awLbg9sTIuEHbgxjLLt7JbeuAQ==" 39 | }, 40 | "node_modules/@next/swc-android-arm-eabi": { 41 | "version": "12.3.2", 42 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz", 43 | "integrity": "sha512-r2rrz+DZ8YYGqzVrbRrpP6GKzwozpOrnFbErc4k36vUTSFMag9yQahZfaBe06JYdqu/e5yhm/saIDEaSVPRP4g==", 44 | "cpu": [ 45 | "arm" 46 | ], 47 | "optional": true, 48 | "os": [ 49 | "android" 50 | ], 51 | "engines": { 52 | "node": ">= 10" 53 | } 54 | }, 55 | "node_modules/@next/swc-android-arm64": { 56 | "version": "12.3.2", 57 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.2.tgz", 58 | "integrity": "sha512-B+TINJhCf+CrY1+b3/JWQlkecv53rAGa/gA7gi5B1cnBa/2Uvoe+Ue0JeCefTjfiyl1ScsyNx+NcESY8Ye2Ngg==", 59 | "cpu": [ 60 | "arm64" 61 | ], 62 | "optional": true, 63 | "os": [ 64 | "android" 65 | ], 66 | "engines": { 67 | "node": ">= 10" 68 | } 69 | }, 70 | "node_modules/@next/swc-darwin-arm64": { 71 | "version": "12.3.2", 72 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.2.tgz", 73 | "integrity": "sha512-PTUfe1ZrwjsiuTmr3bOM9lsoy5DCmfYsLOUF9ZVhtbi5MNJVmUTy4VZ06GfrvnCO5hGCr48z3vpFE9QZ0qLcPw==", 74 | "cpu": [ 75 | "arm64" 76 | ], 77 | "optional": true, 78 | "os": [ 79 | "darwin" 80 | ], 81 | "engines": { 82 | "node": ">= 10" 83 | } 84 | }, 85 | "node_modules/@next/swc-darwin-x64": { 86 | "version": "12.3.2", 87 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz", 88 | "integrity": "sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w==", 89 | "cpu": [ 90 | "x64" 91 | ], 92 | "optional": true, 93 | "os": [ 94 | "darwin" 95 | ], 96 | "engines": { 97 | "node": ">= 10" 98 | } 99 | }, 100 | "node_modules/@next/swc-freebsd-x64": { 101 | "version": "12.3.2", 102 | "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz", 103 | "integrity": "sha512-h5Mx0BKDCJ5Vu/U8e07esF6PjPv1EJgmRbYWTUZMAflu13MQpCJkKEJir7+BeRfTXRfgFf+llc7uocrpd7mcrg==", 104 | "cpu": [ 105 | "x64" 106 | ], 107 | "optional": true, 108 | "os": [ 109 | "freebsd" 110 | ], 111 | "engines": { 112 | "node": ">= 10" 113 | } 114 | }, 115 | "node_modules/@next/swc-linux-arm-gnueabihf": { 116 | "version": "12.3.2", 117 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.2.tgz", 118 | "integrity": "sha512-EuRZAamoxfe/WoWRaC0zsCAoE4gs/mEhilcloNM4J5Mnb3PLY8PZV394W7t5tjBjItMCF7l2Ebwjwtm46tq2RA==", 119 | "cpu": [ 120 | "arm" 121 | ], 122 | "optional": true, 123 | "os": [ 124 | "linux" 125 | ], 126 | "engines": { 127 | "node": ">= 10" 128 | } 129 | }, 130 | "node_modules/@next/swc-linux-arm64-gnu": { 131 | "version": "12.3.2", 132 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz", 133 | "integrity": "sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA==", 134 | "cpu": [ 135 | "arm64" 136 | ], 137 | "optional": true, 138 | "os": [ 139 | "linux" 140 | ], 141 | "engines": { 142 | "node": ">= 10" 143 | } 144 | }, 145 | "node_modules/@next/swc-linux-arm64-musl": { 146 | "version": "12.3.2", 147 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz", 148 | "integrity": "sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw==", 149 | "cpu": [ 150 | "arm64" 151 | ], 152 | "optional": true, 153 | "os": [ 154 | "linux" 155 | ], 156 | "engines": { 157 | "node": ">= 10" 158 | } 159 | }, 160 | "node_modules/@next/swc-linux-x64-gnu": { 161 | "version": "12.3.2", 162 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz", 163 | "integrity": "sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw==", 164 | "cpu": [ 165 | "x64" 166 | ], 167 | "optional": true, 168 | "os": [ 169 | "linux" 170 | ], 171 | "engines": { 172 | "node": ">= 10" 173 | } 174 | }, 175 | "node_modules/@next/swc-linux-x64-musl": { 176 | "version": "12.3.2", 177 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz", 178 | "integrity": "sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw==", 179 | "cpu": [ 180 | "x64" 181 | ], 182 | "optional": true, 183 | "os": [ 184 | "linux" 185 | ], 186 | "engines": { 187 | "node": ">= 10" 188 | } 189 | }, 190 | "node_modules/@next/swc-win32-arm64-msvc": { 191 | "version": "12.3.2", 192 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz", 193 | "integrity": "sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw==", 194 | "cpu": [ 195 | "arm64" 196 | ], 197 | "optional": true, 198 | "os": [ 199 | "win32" 200 | ], 201 | "engines": { 202 | "node": ">= 10" 203 | } 204 | }, 205 | "node_modules/@next/swc-win32-ia32-msvc": { 206 | "version": "12.3.2", 207 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz", 208 | "integrity": "sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g==", 209 | "cpu": [ 210 | "ia32" 211 | ], 212 | "optional": true, 213 | "os": [ 214 | "win32" 215 | ], 216 | "engines": { 217 | "node": ">= 10" 218 | } 219 | }, 220 | "node_modules/@next/swc-win32-x64-msvc": { 221 | "version": "12.3.2", 222 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz", 223 | "integrity": "sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg==", 224 | "cpu": [ 225 | "x64" 226 | ], 227 | "optional": true, 228 | "os": [ 229 | "win32" 230 | ], 231 | "engines": { 232 | "node": ">= 10" 233 | } 234 | }, 235 | "node_modules/@swc/helpers": { 236 | "version": "0.4.11", 237 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", 238 | "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", 239 | "dependencies": { 240 | "tslib": "^2.4.0" 241 | } 242 | }, 243 | "node_modules/caniuse-lite": { 244 | "version": "1.0.30001431", 245 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", 246 | "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", 247 | "funding": [ 248 | { 249 | "type": "opencollective", 250 | "url": "https://opencollective.com/browserslist" 251 | }, 252 | { 253 | "type": "tidelift", 254 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 255 | } 256 | ] 257 | }, 258 | "node_modules/js-tokens": { 259 | "version": "4.0.0", 260 | "license": "MIT", 261 | "peer": true 262 | }, 263 | "node_modules/loose-envify": { 264 | "version": "1.4.0", 265 | "license": "MIT", 266 | "peer": true, 267 | "dependencies": { 268 | "js-tokens": "^3.0.0 || ^4.0.0" 269 | }, 270 | "bin": { 271 | "loose-envify": "cli.js" 272 | } 273 | }, 274 | "node_modules/nanoid": { 275 | "version": "3.3.4", 276 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 277 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", 278 | "bin": { 279 | "nanoid": "bin/nanoid.cjs" 280 | }, 281 | "engines": { 282 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 283 | } 284 | }, 285 | "node_modules/next": { 286 | "version": "12.3.2", 287 | "resolved": "https://registry.npmjs.org/next/-/next-12.3.2.tgz", 288 | "integrity": "sha512-orzvvebCwOqaz1eA5ZA0R5dbKxqtJyw7yeig7kDspu6p8OrplfyelzpvMHcDTKscv/l0nn/0l0v3mSsE8w4k7A==", 289 | "dependencies": { 290 | "@next/env": "12.3.2", 291 | "@swc/helpers": "0.4.11", 292 | "caniuse-lite": "^1.0.30001406", 293 | "postcss": "8.4.14", 294 | "styled-jsx": "5.0.7", 295 | "use-sync-external-store": "1.2.0" 296 | }, 297 | "bin": { 298 | "next": "dist/bin/next" 299 | }, 300 | "engines": { 301 | "node": ">=12.22.0" 302 | }, 303 | "optionalDependencies": { 304 | "@next/swc-android-arm-eabi": "12.3.2", 305 | "@next/swc-android-arm64": "12.3.2", 306 | "@next/swc-darwin-arm64": "12.3.2", 307 | "@next/swc-darwin-x64": "12.3.2", 308 | "@next/swc-freebsd-x64": "12.3.2", 309 | "@next/swc-linux-arm-gnueabihf": "12.3.2", 310 | "@next/swc-linux-arm64-gnu": "12.3.2", 311 | "@next/swc-linux-arm64-musl": "12.3.2", 312 | "@next/swc-linux-x64-gnu": "12.3.2", 313 | "@next/swc-linux-x64-musl": "12.3.2", 314 | "@next/swc-win32-arm64-msvc": "12.3.2", 315 | "@next/swc-win32-ia32-msvc": "12.3.2", 316 | "@next/swc-win32-x64-msvc": "12.3.2" 317 | }, 318 | "peerDependencies": { 319 | "fibers": ">= 3.1.0", 320 | "node-sass": "^6.0.0 || ^7.0.0", 321 | "react": "^17.0.2 || ^18.0.0-0", 322 | "react-dom": "^17.0.2 || ^18.0.0-0", 323 | "sass": "^1.3.0" 324 | }, 325 | "peerDependenciesMeta": { 326 | "fibers": { 327 | "optional": true 328 | }, 329 | "node-sass": { 330 | "optional": true 331 | }, 332 | "sass": { 333 | "optional": true 334 | } 335 | } 336 | }, 337 | "node_modules/next-router-mock": { 338 | "resolved": "../..", 339 | "link": true 340 | }, 341 | "node_modules/picocolors": { 342 | "version": "1.0.0", 343 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 344 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 345 | }, 346 | "node_modules/postcss": { 347 | "version": "8.4.14", 348 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 349 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 350 | "funding": [ 351 | { 352 | "type": "opencollective", 353 | "url": "https://opencollective.com/postcss/" 354 | }, 355 | { 356 | "type": "tidelift", 357 | "url": "https://tidelift.com/funding/github/npm/postcss" 358 | } 359 | ], 360 | "dependencies": { 361 | "nanoid": "^3.3.4", 362 | "picocolors": "^1.0.0", 363 | "source-map-js": "^1.0.2" 364 | }, 365 | "engines": { 366 | "node": "^10 || ^12 || >=14" 367 | } 368 | }, 369 | "node_modules/react": { 370 | "version": "18.1.0", 371 | "license": "MIT", 372 | "peer": true, 373 | "dependencies": { 374 | "loose-envify": "^1.1.0" 375 | }, 376 | "engines": { 377 | "node": ">=0.10.0" 378 | } 379 | }, 380 | "node_modules/react-dom": { 381 | "version": "18.1.0", 382 | "license": "MIT", 383 | "peer": true, 384 | "dependencies": { 385 | "loose-envify": "^1.1.0", 386 | "scheduler": "^0.22.0" 387 | }, 388 | "peerDependencies": { 389 | "react": "^18.1.0" 390 | } 391 | }, 392 | "node_modules/scheduler": { 393 | "version": "0.22.0", 394 | "license": "MIT", 395 | "peer": true, 396 | "dependencies": { 397 | "loose-envify": "^1.1.0" 398 | } 399 | }, 400 | "node_modules/source-map-js": { 401 | "version": "1.0.2", 402 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 403 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 404 | "engines": { 405 | "node": ">=0.10.0" 406 | } 407 | }, 408 | "node_modules/styled-jsx": { 409 | "version": "5.0.7", 410 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", 411 | "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", 412 | "engines": { 413 | "node": ">= 12.0.0" 414 | }, 415 | "peerDependencies": { 416 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 417 | }, 418 | "peerDependenciesMeta": { 419 | "@babel/core": { 420 | "optional": true 421 | }, 422 | "babel-plugin-macros": { 423 | "optional": true 424 | } 425 | } 426 | }, 427 | "node_modules/tslib": { 428 | "version": "2.4.1", 429 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 430 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 431 | }, 432 | "node_modules/use-sync-external-store": { 433 | "version": "1.2.0", 434 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 435 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 436 | "peerDependencies": { 437 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 438 | } 439 | } 440 | }, 441 | "dependencies": { 442 | "@next/env": { 443 | "version": "12.3.2", 444 | "resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.2.tgz", 445 | "integrity": "sha512-upwtMaHxlv/udAWGq0kE+rg8huwmcxQPsKZFhS1R5iVO323mvxEBe1YrSXe1awLbg9sTIuEHbgxjLLt7JbeuAQ==" 446 | }, 447 | "@next/swc-android-arm-eabi": { 448 | "version": "12.3.2", 449 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz", 450 | "integrity": "sha512-r2rrz+DZ8YYGqzVrbRrpP6GKzwozpOrnFbErc4k36vUTSFMag9yQahZfaBe06JYdqu/e5yhm/saIDEaSVPRP4g==", 451 | "optional": true 452 | }, 453 | "@next/swc-android-arm64": { 454 | "version": "12.3.2", 455 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.2.tgz", 456 | "integrity": "sha512-B+TINJhCf+CrY1+b3/JWQlkecv53rAGa/gA7gi5B1cnBa/2Uvoe+Ue0JeCefTjfiyl1ScsyNx+NcESY8Ye2Ngg==", 457 | "optional": true 458 | }, 459 | "@next/swc-darwin-arm64": { 460 | "version": "12.3.2", 461 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.2.tgz", 462 | "integrity": "sha512-PTUfe1ZrwjsiuTmr3bOM9lsoy5DCmfYsLOUF9ZVhtbi5MNJVmUTy4VZ06GfrvnCO5hGCr48z3vpFE9QZ0qLcPw==", 463 | "optional": true 464 | }, 465 | "@next/swc-darwin-x64": { 466 | "version": "12.3.2", 467 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz", 468 | "integrity": "sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w==", 469 | "optional": true 470 | }, 471 | "@next/swc-freebsd-x64": { 472 | "version": "12.3.2", 473 | "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz", 474 | "integrity": "sha512-h5Mx0BKDCJ5Vu/U8e07esF6PjPv1EJgmRbYWTUZMAflu13MQpCJkKEJir7+BeRfTXRfgFf+llc7uocrpd7mcrg==", 475 | "optional": true 476 | }, 477 | "@next/swc-linux-arm-gnueabihf": { 478 | "version": "12.3.2", 479 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.2.tgz", 480 | "integrity": "sha512-EuRZAamoxfe/WoWRaC0zsCAoE4gs/mEhilcloNM4J5Mnb3PLY8PZV394W7t5tjBjItMCF7l2Ebwjwtm46tq2RA==", 481 | "optional": true 482 | }, 483 | "@next/swc-linux-arm64-gnu": { 484 | "version": "12.3.2", 485 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz", 486 | "integrity": "sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA==", 487 | "optional": true 488 | }, 489 | "@next/swc-linux-arm64-musl": { 490 | "version": "12.3.2", 491 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz", 492 | "integrity": "sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw==", 493 | "optional": true 494 | }, 495 | "@next/swc-linux-x64-gnu": { 496 | "version": "12.3.2", 497 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz", 498 | "integrity": "sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw==", 499 | "optional": true 500 | }, 501 | "@next/swc-linux-x64-musl": { 502 | "version": "12.3.2", 503 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz", 504 | "integrity": "sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw==", 505 | "optional": true 506 | }, 507 | "@next/swc-win32-arm64-msvc": { 508 | "version": "12.3.2", 509 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz", 510 | "integrity": "sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw==", 511 | "optional": true 512 | }, 513 | "@next/swc-win32-ia32-msvc": { 514 | "version": "12.3.2", 515 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz", 516 | "integrity": "sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g==", 517 | "optional": true 518 | }, 519 | "@next/swc-win32-x64-msvc": { 520 | "version": "12.3.2", 521 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz", 522 | "integrity": "sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg==", 523 | "optional": true 524 | }, 525 | "@swc/helpers": { 526 | "version": "0.4.11", 527 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", 528 | "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", 529 | "requires": { 530 | "tslib": "^2.4.0" 531 | } 532 | }, 533 | "caniuse-lite": { 534 | "version": "1.0.30001431", 535 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", 536 | "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==" 537 | }, 538 | "js-tokens": { 539 | "version": "4.0.0", 540 | "peer": true 541 | }, 542 | "loose-envify": { 543 | "version": "1.4.0", 544 | "peer": true, 545 | "requires": { 546 | "js-tokens": "^3.0.0 || ^4.0.0" 547 | } 548 | }, 549 | "nanoid": { 550 | "version": "3.3.4", 551 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 552 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" 553 | }, 554 | "next": { 555 | "version": "12.3.2", 556 | "resolved": "https://registry.npmjs.org/next/-/next-12.3.2.tgz", 557 | "integrity": "sha512-orzvvebCwOqaz1eA5ZA0R5dbKxqtJyw7yeig7kDspu6p8OrplfyelzpvMHcDTKscv/l0nn/0l0v3mSsE8w4k7A==", 558 | "requires": { 559 | "@next/env": "12.3.2", 560 | "@next/swc-android-arm-eabi": "12.3.2", 561 | "@next/swc-android-arm64": "12.3.2", 562 | "@next/swc-darwin-arm64": "12.3.2", 563 | "@next/swc-darwin-x64": "12.3.2", 564 | "@next/swc-freebsd-x64": "12.3.2", 565 | "@next/swc-linux-arm-gnueabihf": "12.3.2", 566 | "@next/swc-linux-arm64-gnu": "12.3.2", 567 | "@next/swc-linux-arm64-musl": "12.3.2", 568 | "@next/swc-linux-x64-gnu": "12.3.2", 569 | "@next/swc-linux-x64-musl": "12.3.2", 570 | "@next/swc-win32-arm64-msvc": "12.3.2", 571 | "@next/swc-win32-ia32-msvc": "12.3.2", 572 | "@next/swc-win32-x64-msvc": "12.3.2", 573 | "@swc/helpers": "0.4.11", 574 | "caniuse-lite": "^1.0.30001406", 575 | "postcss": "8.4.14", 576 | "styled-jsx": "5.0.7", 577 | "use-sync-external-store": "1.2.0" 578 | } 579 | }, 580 | "next-router-mock": { 581 | "version": "file:../..", 582 | "requires": { 583 | "@changesets/cli": "^2.26.2", 584 | "@testing-library/react": "^13.4.0", 585 | "@types/jest": "^26.0.20", 586 | "doctoc": "^2.2.0", 587 | "jest": "^26.6.3", 588 | "next": "^13.5.1", 589 | "prettier": "^2.2.1", 590 | "react": "^18.2.0", 591 | "react-dom": "^18.2.0", 592 | "react-test-renderer": "^18.2.0", 593 | "rimraf": "^3.0.2", 594 | "ts-jest": "^26.4.4", 595 | "typescript": "^4.9.5" 596 | } 597 | }, 598 | "picocolors": { 599 | "version": "1.0.0", 600 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 601 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 602 | }, 603 | "postcss": { 604 | "version": "8.4.14", 605 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 606 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 607 | "requires": { 608 | "nanoid": "^3.3.4", 609 | "picocolors": "^1.0.0", 610 | "source-map-js": "^1.0.2" 611 | } 612 | }, 613 | "react": { 614 | "version": "18.1.0", 615 | "peer": true, 616 | "requires": { 617 | "loose-envify": "^1.1.0" 618 | } 619 | }, 620 | "react-dom": { 621 | "version": "18.1.0", 622 | "peer": true, 623 | "requires": { 624 | "loose-envify": "^1.1.0", 625 | "scheduler": "^0.22.0" 626 | } 627 | }, 628 | "scheduler": { 629 | "version": "0.22.0", 630 | "peer": true, 631 | "requires": { 632 | "loose-envify": "^1.1.0" 633 | } 634 | }, 635 | "source-map-js": { 636 | "version": "1.0.2", 637 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 638 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 639 | }, 640 | "styled-jsx": { 641 | "version": "5.0.7", 642 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz", 643 | "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==", 644 | "requires": {} 645 | }, 646 | "tslib": { 647 | "version": "2.4.1", 648 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 649 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 650 | }, 651 | "use-sync-external-store": { 652 | "version": "1.2.0", 653 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 654 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 655 | "requires": {} 656 | } 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /test/next-12.2+/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "12.3.2", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-12.latest/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/next-12.latest/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-12.latest/next-12.latest.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-12"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-12"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-12", () => { 15 | require("next-router-mock/dynamic-routes/next-12"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-12", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-12"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-12.latest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "12.*", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-12/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-12/next-12.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-12"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-12"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-12", () => { 15 | require("next-router-mock/dynamic-routes/next-12"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-12", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-12"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-12/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-12", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "next": "12.1.6", 9 | "next-router-mock": "../.." 10 | } 11 | }, 12 | "../..": { 13 | "version": "1.0.0", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@changesets/cli": "^2.26.2", 17 | "@testing-library/react": "^13.4.0", 18 | "@types/jest": "^26.0.20", 19 | "doctoc": "^2.2.0", 20 | "jest": "^26.6.3", 21 | "next": "^13.5.1", 22 | "prettier": "^2.2.1", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-test-renderer": "^18.2.0", 26 | "rimraf": "^3.0.2", 27 | "ts-jest": "^26.4.4", 28 | "typescript": "^4.9.5" 29 | }, 30 | "peerDependencies": { 31 | "next": ">=10.0.0", 32 | "react": ">=17.0.0" 33 | } 34 | }, 35 | "node_modules/@next/env": { 36 | "version": "12.1.6", 37 | "license": "MIT" 38 | }, 39 | "node_modules/@next/swc-darwin-x64": { 40 | "version": "12.1.6", 41 | "cpu": [ 42 | "x64" 43 | ], 44 | "license": "MIT", 45 | "optional": true, 46 | "os": [ 47 | "darwin" 48 | ], 49 | "engines": { 50 | "node": ">= 10" 51 | } 52 | }, 53 | "node_modules/caniuse-lite": { 54 | "version": "1.0.30001344", 55 | "funding": [ 56 | { 57 | "type": "opencollective", 58 | "url": "https://opencollective.com/browserslist" 59 | }, 60 | { 61 | "type": "tidelift", 62 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 63 | } 64 | ], 65 | "license": "CC-BY-4.0" 66 | }, 67 | "node_modules/js-tokens": { 68 | "version": "4.0.0", 69 | "license": "MIT", 70 | "peer": true 71 | }, 72 | "node_modules/loose-envify": { 73 | "version": "1.4.0", 74 | "license": "MIT", 75 | "peer": true, 76 | "dependencies": { 77 | "js-tokens": "^3.0.0 || ^4.0.0" 78 | }, 79 | "bin": { 80 | "loose-envify": "cli.js" 81 | } 82 | }, 83 | "node_modules/nanoid": { 84 | "version": "3.3.4", 85 | "license": "MIT", 86 | "bin": { 87 | "nanoid": "bin/nanoid.cjs" 88 | }, 89 | "engines": { 90 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 91 | } 92 | }, 93 | "node_modules/next": { 94 | "version": "12.1.6", 95 | "license": "MIT", 96 | "dependencies": { 97 | "@next/env": "12.1.6", 98 | "caniuse-lite": "^1.0.30001332", 99 | "postcss": "8.4.5", 100 | "styled-jsx": "5.0.2" 101 | }, 102 | "bin": { 103 | "next": "dist/bin/next" 104 | }, 105 | "engines": { 106 | "node": ">=12.22.0" 107 | }, 108 | "optionalDependencies": { 109 | "@next/swc-android-arm-eabi": "12.1.6", 110 | "@next/swc-android-arm64": "12.1.6", 111 | "@next/swc-darwin-arm64": "12.1.6", 112 | "@next/swc-darwin-x64": "12.1.6", 113 | "@next/swc-linux-arm-gnueabihf": "12.1.6", 114 | "@next/swc-linux-arm64-gnu": "12.1.6", 115 | "@next/swc-linux-arm64-musl": "12.1.6", 116 | "@next/swc-linux-x64-gnu": "12.1.6", 117 | "@next/swc-linux-x64-musl": "12.1.6", 118 | "@next/swc-win32-arm64-msvc": "12.1.6", 119 | "@next/swc-win32-ia32-msvc": "12.1.6", 120 | "@next/swc-win32-x64-msvc": "12.1.6" 121 | }, 122 | "peerDependencies": { 123 | "fibers": ">= 3.1.0", 124 | "node-sass": "^6.0.0 || ^7.0.0", 125 | "react": "^17.0.2 || ^18.0.0-0", 126 | "react-dom": "^17.0.2 || ^18.0.0-0", 127 | "sass": "^1.3.0" 128 | }, 129 | "peerDependenciesMeta": { 130 | "fibers": { 131 | "optional": true 132 | }, 133 | "node-sass": { 134 | "optional": true 135 | }, 136 | "sass": { 137 | "optional": true 138 | } 139 | } 140 | }, 141 | "node_modules/next-router-mock": { 142 | "resolved": "../..", 143 | "link": true 144 | }, 145 | "node_modules/picocolors": { 146 | "version": "1.0.0", 147 | "license": "ISC" 148 | }, 149 | "node_modules/postcss": { 150 | "version": "8.4.5", 151 | "license": "MIT", 152 | "dependencies": { 153 | "nanoid": "^3.1.30", 154 | "picocolors": "^1.0.0", 155 | "source-map-js": "^1.0.1" 156 | }, 157 | "engines": { 158 | "node": "^10 || ^12 || >=14" 159 | }, 160 | "funding": { 161 | "type": "opencollective", 162 | "url": "https://opencollective.com/postcss/" 163 | } 164 | }, 165 | "node_modules/react": { 166 | "version": "18.1.0", 167 | "license": "MIT", 168 | "peer": true, 169 | "dependencies": { 170 | "loose-envify": "^1.1.0" 171 | }, 172 | "engines": { 173 | "node": ">=0.10.0" 174 | } 175 | }, 176 | "node_modules/react-dom": { 177 | "version": "18.1.0", 178 | "license": "MIT", 179 | "peer": true, 180 | "dependencies": { 181 | "loose-envify": "^1.1.0", 182 | "scheduler": "^0.22.0" 183 | }, 184 | "peerDependencies": { 185 | "react": "^18.1.0" 186 | } 187 | }, 188 | "node_modules/scheduler": { 189 | "version": "0.22.0", 190 | "license": "MIT", 191 | "peer": true, 192 | "dependencies": { 193 | "loose-envify": "^1.1.0" 194 | } 195 | }, 196 | "node_modules/source-map-js": { 197 | "version": "1.0.2", 198 | "license": "BSD-3-Clause", 199 | "engines": { 200 | "node": ">=0.10.0" 201 | } 202 | }, 203 | "node_modules/styled-jsx": { 204 | "version": "5.0.2", 205 | "license": "MIT", 206 | "engines": { 207 | "node": ">= 12.0.0" 208 | }, 209 | "peerDependencies": { 210 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 211 | }, 212 | "peerDependenciesMeta": { 213 | "@babel/core": { 214 | "optional": true 215 | }, 216 | "babel-plugin-macros": { 217 | "optional": true 218 | } 219 | } 220 | } 221 | }, 222 | "dependencies": { 223 | "@next/env": { 224 | "version": "12.1.6" 225 | }, 226 | "@next/swc-darwin-x64": { 227 | "version": "12.1.6", 228 | "optional": true 229 | }, 230 | "caniuse-lite": { 231 | "version": "1.0.30001344" 232 | }, 233 | "js-tokens": { 234 | "version": "4.0.0", 235 | "peer": true 236 | }, 237 | "loose-envify": { 238 | "version": "1.4.0", 239 | "peer": true, 240 | "requires": { 241 | "js-tokens": "^3.0.0 || ^4.0.0" 242 | } 243 | }, 244 | "nanoid": { 245 | "version": "3.3.4" 246 | }, 247 | "next": { 248 | "version": "12.1.6", 249 | "requires": { 250 | "@next/env": "12.1.6", 251 | "@next/swc-android-arm-eabi": "12.1.6", 252 | "@next/swc-android-arm64": "12.1.6", 253 | "@next/swc-darwin-arm64": "12.1.6", 254 | "@next/swc-darwin-x64": "12.1.6", 255 | "@next/swc-linux-arm-gnueabihf": "12.1.6", 256 | "@next/swc-linux-arm64-gnu": "12.1.6", 257 | "@next/swc-linux-arm64-musl": "12.1.6", 258 | "@next/swc-linux-x64-gnu": "12.1.6", 259 | "@next/swc-linux-x64-musl": "12.1.6", 260 | "@next/swc-win32-arm64-msvc": "12.1.6", 261 | "@next/swc-win32-ia32-msvc": "12.1.6", 262 | "@next/swc-win32-x64-msvc": "12.1.6", 263 | "caniuse-lite": "^1.0.30001332", 264 | "postcss": "8.4.5", 265 | "styled-jsx": "5.0.2" 266 | } 267 | }, 268 | "next-router-mock": { 269 | "version": "file:../..", 270 | "requires": { 271 | "@changesets/cli": "^2.26.2", 272 | "@testing-library/react": "^13.4.0", 273 | "@types/jest": "^26.0.20", 274 | "doctoc": "^2.2.0", 275 | "jest": "^26.6.3", 276 | "next": "^13.5.1", 277 | "prettier": "^2.2.1", 278 | "react": "^18.2.0", 279 | "react-dom": "^18.2.0", 280 | "react-test-renderer": "^18.2.0", 281 | "rimraf": "^3.0.2", 282 | "ts-jest": "^26.4.4", 283 | "typescript": "^4.9.5" 284 | } 285 | }, 286 | "picocolors": { 287 | "version": "1.0.0" 288 | }, 289 | "postcss": { 290 | "version": "8.4.5", 291 | "requires": { 292 | "nanoid": "^3.1.30", 293 | "picocolors": "^1.0.0", 294 | "source-map-js": "^1.0.1" 295 | } 296 | }, 297 | "react": { 298 | "version": "18.1.0", 299 | "peer": true, 300 | "requires": { 301 | "loose-envify": "^1.1.0" 302 | } 303 | }, 304 | "react-dom": { 305 | "version": "18.1.0", 306 | "peer": true, 307 | "requires": { 308 | "loose-envify": "^1.1.0", 309 | "scheduler": "^0.22.0" 310 | } 311 | }, 312 | "scheduler": { 313 | "version": "0.22.0", 314 | "peer": true, 315 | "requires": { 316 | "loose-envify": "^1.1.0" 317 | } 318 | }, 319 | "source-map-js": { 320 | "version": "1.0.2" 321 | }, 322 | "styled-jsx": { 323 | "version": "5.0.2", 324 | "requires": {} 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /test/next-12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "12.1.6", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-13.5/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-13.5/next-13.5.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-13"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-13.5"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-13", () => { 15 | require("next-router-mock/dynamic-routes/next-13"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-13", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-13.5"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-13.5/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-13.5", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "next": "13.5.1", 9 | "next-router-mock": "../.." 10 | } 11 | }, 12 | "../..": { 13 | "version": "1.0.0", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@changesets/cli": "^2.26.2", 17 | "@testing-library/react": "^13.4.0", 18 | "@types/jest": "^26.0.20", 19 | "doctoc": "^2.2.0", 20 | "jest": "^26.6.3", 21 | "next": "^13.5.1", 22 | "prettier": "^2.2.1", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-test-renderer": "^18.2.0", 26 | "rimraf": "^3.0.2", 27 | "ts-jest": "^26.4.4", 28 | "typescript": "^4.9.5" 29 | }, 30 | "peerDependencies": { 31 | "next": ">=10.0.0", 32 | "react": ">=17.0.0" 33 | } 34 | }, 35 | "node_modules/@next/env": { 36 | "version": "13.5.1", 37 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.1.tgz", 38 | "integrity": "sha512-CIMWiOTyflFn/GFx33iYXkgLSQsMQZV4jB91qaj/TfxGaGOXxn8C1j72TaUSPIyN7ziS/AYG46kGmnvuk1oOpg==" 39 | }, 40 | "node_modules/@next/swc-darwin-arm64": { 41 | "version": "13.5.1", 42 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.1.tgz", 43 | "integrity": "sha512-Bcd0VFrLHZnMmJy6LqV1CydZ7lYaBao8YBEdQUVzV8Ypn/l5s//j5ffjfvMzpEQ4mzlAj3fIY+Bmd9NxpWhACw==", 44 | "cpu": [ 45 | "arm64" 46 | ], 47 | "optional": true, 48 | "os": [ 49 | "darwin" 50 | ], 51 | "engines": { 52 | "node": ">= 10" 53 | } 54 | }, 55 | "node_modules/@next/swc-darwin-x64": { 56 | "version": "13.5.1", 57 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.1.tgz", 58 | "integrity": "sha512-uvTZrZa4D0bdWa1jJ7X1tBGIxzpqSnw/ATxWvoRO9CVBvXSx87JyuISY+BWsfLFF59IRodESdeZwkWM2l6+Kjg==", 59 | "cpu": [ 60 | "x64" 61 | ], 62 | "optional": true, 63 | "os": [ 64 | "darwin" 65 | ], 66 | "engines": { 67 | "node": ">= 10" 68 | } 69 | }, 70 | "node_modules/@next/swc-linux-arm64-gnu": { 71 | "version": "13.5.1", 72 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.1.tgz", 73 | "integrity": "sha512-/52ThlqdORPQt3+AlMoO+omicdYyUEDeRDGPAj86ULpV4dg+/GCFCKAmFWT0Q4zChFwsAoZUECLcKbRdcc0SNg==", 74 | "cpu": [ 75 | "arm64" 76 | ], 77 | "optional": true, 78 | "os": [ 79 | "linux" 80 | ], 81 | "engines": { 82 | "node": ">= 10" 83 | } 84 | }, 85 | "node_modules/@next/swc-linux-arm64-musl": { 86 | "version": "13.5.1", 87 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.1.tgz", 88 | "integrity": "sha512-L4qNXSOHeu1hEAeeNsBgIYVnvm0gg9fj2O2Yx/qawgQEGuFBfcKqlmIE/Vp8z6gwlppxz5d7v6pmHs1NB6R37w==", 89 | "cpu": [ 90 | "arm64" 91 | ], 92 | "optional": true, 93 | "os": [ 94 | "linux" 95 | ], 96 | "engines": { 97 | "node": ">= 10" 98 | } 99 | }, 100 | "node_modules/@next/swc-linux-x64-gnu": { 101 | "version": "13.5.1", 102 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.1.tgz", 103 | "integrity": "sha512-QVvMrlrFFYvLtABk092kcZ5Mzlmsk2+SV3xYuAu8sbTuIoh0U2+HGNhVklmuYCuM3DAAxdiMQTNlRQmNH11udw==", 104 | "cpu": [ 105 | "x64" 106 | ], 107 | "optional": true, 108 | "os": [ 109 | "linux" 110 | ], 111 | "engines": { 112 | "node": ">= 10" 113 | } 114 | }, 115 | "node_modules/@next/swc-linux-x64-musl": { 116 | "version": "13.5.1", 117 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.1.tgz", 118 | "integrity": "sha512-bBnr+XuWc28r9e8gQ35XBtyi5KLHLhTbEvrSgcWna8atI48sNggjIK8IyiEBO3KIrcUVXYkldAzGXPEYMnKt1g==", 119 | "cpu": [ 120 | "x64" 121 | ], 122 | "optional": true, 123 | "os": [ 124 | "linux" 125 | ], 126 | "engines": { 127 | "node": ">= 10" 128 | } 129 | }, 130 | "node_modules/@next/swc-win32-arm64-msvc": { 131 | "version": "13.5.1", 132 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.1.tgz", 133 | "integrity": "sha512-EQGeE4S5c9v06jje9gr4UlxqUEA+zrsgPi6kg9VwR+dQHirzbnVJISF69UfKVkmLntknZJJI9XpWPB6q0Z7mTg==", 134 | "cpu": [ 135 | "arm64" 136 | ], 137 | "optional": true, 138 | "os": [ 139 | "win32" 140 | ], 141 | "engines": { 142 | "node": ">= 10" 143 | } 144 | }, 145 | "node_modules/@next/swc-win32-ia32-msvc": { 146 | "version": "13.5.1", 147 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.1.tgz", 148 | "integrity": "sha512-1y31Q6awzofVjmbTLtRl92OX3s+W0ZfO8AP8fTnITcIo9a6ATDc/eqa08fd6tSpFu6IFpxOBbdevOjwYTGx/AQ==", 149 | "cpu": [ 150 | "ia32" 151 | ], 152 | "optional": true, 153 | "os": [ 154 | "win32" 155 | ], 156 | "engines": { 157 | "node": ">= 10" 158 | } 159 | }, 160 | "node_modules/@next/swc-win32-x64-msvc": { 161 | "version": "13.5.1", 162 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.1.tgz", 163 | "integrity": "sha512-+9XBQizy7X/GuwNegq+5QkkxAPV7SBsIwapVRQd9WSvvU20YO23B3bZUpevdabi4fsd25y9RJDDncljy/V54ww==", 164 | "cpu": [ 165 | "x64" 166 | ], 167 | "optional": true, 168 | "os": [ 169 | "win32" 170 | ], 171 | "engines": { 172 | "node": ">= 10" 173 | } 174 | }, 175 | "node_modules/@swc/helpers": { 176 | "version": "0.5.2", 177 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", 178 | "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", 179 | "dependencies": { 180 | "tslib": "^2.4.0" 181 | } 182 | }, 183 | "node_modules/busboy": { 184 | "version": "1.6.0", 185 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 186 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 187 | "dependencies": { 188 | "streamsearch": "^1.1.0" 189 | }, 190 | "engines": { 191 | "node": ">=10.16.0" 192 | } 193 | }, 194 | "node_modules/caniuse-lite": { 195 | "version": "1.0.30001431", 196 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", 197 | "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", 198 | "funding": [ 199 | { 200 | "type": "opencollective", 201 | "url": "https://opencollective.com/browserslist" 202 | }, 203 | { 204 | "type": "tidelift", 205 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 206 | } 207 | ] 208 | }, 209 | "node_modules/client-only": { 210 | "version": "0.0.1", 211 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 212 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 213 | }, 214 | "node_modules/glob-to-regexp": { 215 | "version": "0.4.1", 216 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 217 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 218 | }, 219 | "node_modules/graceful-fs": { 220 | "version": "4.2.11", 221 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 222 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 223 | }, 224 | "node_modules/js-tokens": { 225 | "version": "4.0.0", 226 | "license": "MIT", 227 | "peer": true 228 | }, 229 | "node_modules/loose-envify": { 230 | "version": "1.4.0", 231 | "license": "MIT", 232 | "peer": true, 233 | "dependencies": { 234 | "js-tokens": "^3.0.0 || ^4.0.0" 235 | }, 236 | "bin": { 237 | "loose-envify": "cli.js" 238 | } 239 | }, 240 | "node_modules/nanoid": { 241 | "version": "3.3.4", 242 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 243 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", 244 | "bin": { 245 | "nanoid": "bin/nanoid.cjs" 246 | }, 247 | "engines": { 248 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 249 | } 250 | }, 251 | "node_modules/next": { 252 | "version": "13.5.1", 253 | "resolved": "https://registry.npmjs.org/next/-/next-13.5.1.tgz", 254 | "integrity": "sha512-GIudNR7ggGUZoIL79mSZcxbXK9f5pwAIPZxEM8+j2yLqv5RODg4TkmUlaKSYVqE1bPQueamXSqdC3j7axiTSEg==", 255 | "dependencies": { 256 | "@next/env": "13.5.1", 257 | "@swc/helpers": "0.5.2", 258 | "busboy": "1.6.0", 259 | "caniuse-lite": "^1.0.30001406", 260 | "postcss": "8.4.14", 261 | "styled-jsx": "5.1.1", 262 | "watchpack": "2.4.0", 263 | "zod": "3.21.4" 264 | }, 265 | "bin": { 266 | "next": "dist/bin/next" 267 | }, 268 | "engines": { 269 | "node": ">=16.14.0" 270 | }, 271 | "optionalDependencies": { 272 | "@next/swc-darwin-arm64": "13.5.1", 273 | "@next/swc-darwin-x64": "13.5.1", 274 | "@next/swc-linux-arm64-gnu": "13.5.1", 275 | "@next/swc-linux-arm64-musl": "13.5.1", 276 | "@next/swc-linux-x64-gnu": "13.5.1", 277 | "@next/swc-linux-x64-musl": "13.5.1", 278 | "@next/swc-win32-arm64-msvc": "13.5.1", 279 | "@next/swc-win32-ia32-msvc": "13.5.1", 280 | "@next/swc-win32-x64-msvc": "13.5.1" 281 | }, 282 | "peerDependencies": { 283 | "@opentelemetry/api": "^1.1.0", 284 | "react": "^18.2.0", 285 | "react-dom": "^18.2.0", 286 | "sass": "^1.3.0" 287 | }, 288 | "peerDependenciesMeta": { 289 | "@opentelemetry/api": { 290 | "optional": true 291 | }, 292 | "sass": { 293 | "optional": true 294 | } 295 | } 296 | }, 297 | "node_modules/next-router-mock": { 298 | "resolved": "../..", 299 | "link": true 300 | }, 301 | "node_modules/picocolors": { 302 | "version": "1.0.0", 303 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 304 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 305 | }, 306 | "node_modules/postcss": { 307 | "version": "8.4.14", 308 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 309 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 310 | "funding": [ 311 | { 312 | "type": "opencollective", 313 | "url": "https://opencollective.com/postcss/" 314 | }, 315 | { 316 | "type": "tidelift", 317 | "url": "https://tidelift.com/funding/github/npm/postcss" 318 | } 319 | ], 320 | "dependencies": { 321 | "nanoid": "^3.3.4", 322 | "picocolors": "^1.0.0", 323 | "source-map-js": "^1.0.2" 324 | }, 325 | "engines": { 326 | "node": "^10 || ^12 || >=14" 327 | } 328 | }, 329 | "node_modules/react": { 330 | "version": "18.2.0", 331 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 332 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 333 | "peer": true, 334 | "dependencies": { 335 | "loose-envify": "^1.1.0" 336 | }, 337 | "engines": { 338 | "node": ">=0.10.0" 339 | } 340 | }, 341 | "node_modules/react-dom": { 342 | "version": "18.2.0", 343 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 344 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 345 | "peer": true, 346 | "dependencies": { 347 | "loose-envify": "^1.1.0", 348 | "scheduler": "^0.23.0" 349 | }, 350 | "peerDependencies": { 351 | "react": "^18.2.0" 352 | } 353 | }, 354 | "node_modules/scheduler": { 355 | "version": "0.23.0", 356 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 357 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 358 | "peer": true, 359 | "dependencies": { 360 | "loose-envify": "^1.1.0" 361 | } 362 | }, 363 | "node_modules/source-map-js": { 364 | "version": "1.0.2", 365 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 366 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 367 | "engines": { 368 | "node": ">=0.10.0" 369 | } 370 | }, 371 | "node_modules/streamsearch": { 372 | "version": "1.1.0", 373 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 374 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 375 | "engines": { 376 | "node": ">=10.0.0" 377 | } 378 | }, 379 | "node_modules/styled-jsx": { 380 | "version": "5.1.1", 381 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 382 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 383 | "dependencies": { 384 | "client-only": "0.0.1" 385 | }, 386 | "engines": { 387 | "node": ">= 12.0.0" 388 | }, 389 | "peerDependencies": { 390 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 391 | }, 392 | "peerDependenciesMeta": { 393 | "@babel/core": { 394 | "optional": true 395 | }, 396 | "babel-plugin-macros": { 397 | "optional": true 398 | } 399 | } 400 | }, 401 | "node_modules/tslib": { 402 | "version": "2.6.2", 403 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 404 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 405 | }, 406 | "node_modules/watchpack": { 407 | "version": "2.4.0", 408 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", 409 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", 410 | "dependencies": { 411 | "glob-to-regexp": "^0.4.1", 412 | "graceful-fs": "^4.1.2" 413 | }, 414 | "engines": { 415 | "node": ">=10.13.0" 416 | } 417 | }, 418 | "node_modules/zod": { 419 | "version": "3.21.4", 420 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", 421 | "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", 422 | "funding": { 423 | "url": "https://github.com/sponsors/colinhacks" 424 | } 425 | } 426 | }, 427 | "dependencies": { 428 | "@next/env": { 429 | "version": "13.5.1", 430 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.1.tgz", 431 | "integrity": "sha512-CIMWiOTyflFn/GFx33iYXkgLSQsMQZV4jB91qaj/TfxGaGOXxn8C1j72TaUSPIyN7ziS/AYG46kGmnvuk1oOpg==" 432 | }, 433 | "@next/swc-darwin-arm64": { 434 | "version": "13.5.1", 435 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.1.tgz", 436 | "integrity": "sha512-Bcd0VFrLHZnMmJy6LqV1CydZ7lYaBao8YBEdQUVzV8Ypn/l5s//j5ffjfvMzpEQ4mzlAj3fIY+Bmd9NxpWhACw==", 437 | "optional": true 438 | }, 439 | "@next/swc-darwin-x64": { 440 | "version": "13.5.1", 441 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.1.tgz", 442 | "integrity": "sha512-uvTZrZa4D0bdWa1jJ7X1tBGIxzpqSnw/ATxWvoRO9CVBvXSx87JyuISY+BWsfLFF59IRodESdeZwkWM2l6+Kjg==", 443 | "optional": true 444 | }, 445 | "@next/swc-linux-arm64-gnu": { 446 | "version": "13.5.1", 447 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.1.tgz", 448 | "integrity": "sha512-/52ThlqdORPQt3+AlMoO+omicdYyUEDeRDGPAj86ULpV4dg+/GCFCKAmFWT0Q4zChFwsAoZUECLcKbRdcc0SNg==", 449 | "optional": true 450 | }, 451 | "@next/swc-linux-arm64-musl": { 452 | "version": "13.5.1", 453 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.1.tgz", 454 | "integrity": "sha512-L4qNXSOHeu1hEAeeNsBgIYVnvm0gg9fj2O2Yx/qawgQEGuFBfcKqlmIE/Vp8z6gwlppxz5d7v6pmHs1NB6R37w==", 455 | "optional": true 456 | }, 457 | "@next/swc-linux-x64-gnu": { 458 | "version": "13.5.1", 459 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.1.tgz", 460 | "integrity": "sha512-QVvMrlrFFYvLtABk092kcZ5Mzlmsk2+SV3xYuAu8sbTuIoh0U2+HGNhVklmuYCuM3DAAxdiMQTNlRQmNH11udw==", 461 | "optional": true 462 | }, 463 | "@next/swc-linux-x64-musl": { 464 | "version": "13.5.1", 465 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.1.tgz", 466 | "integrity": "sha512-bBnr+XuWc28r9e8gQ35XBtyi5KLHLhTbEvrSgcWna8atI48sNggjIK8IyiEBO3KIrcUVXYkldAzGXPEYMnKt1g==", 467 | "optional": true 468 | }, 469 | "@next/swc-win32-arm64-msvc": { 470 | "version": "13.5.1", 471 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.1.tgz", 472 | "integrity": "sha512-EQGeE4S5c9v06jje9gr4UlxqUEA+zrsgPi6kg9VwR+dQHirzbnVJISF69UfKVkmLntknZJJI9XpWPB6q0Z7mTg==", 473 | "optional": true 474 | }, 475 | "@next/swc-win32-ia32-msvc": { 476 | "version": "13.5.1", 477 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.1.tgz", 478 | "integrity": "sha512-1y31Q6awzofVjmbTLtRl92OX3s+W0ZfO8AP8fTnITcIo9a6ATDc/eqa08fd6tSpFu6IFpxOBbdevOjwYTGx/AQ==", 479 | "optional": true 480 | }, 481 | "@next/swc-win32-x64-msvc": { 482 | "version": "13.5.1", 483 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.1.tgz", 484 | "integrity": "sha512-+9XBQizy7X/GuwNegq+5QkkxAPV7SBsIwapVRQd9WSvvU20YO23B3bZUpevdabi4fsd25y9RJDDncljy/V54ww==", 485 | "optional": true 486 | }, 487 | "@swc/helpers": { 488 | "version": "0.5.2", 489 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", 490 | "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", 491 | "requires": { 492 | "tslib": "^2.4.0" 493 | } 494 | }, 495 | "busboy": { 496 | "version": "1.6.0", 497 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 498 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 499 | "requires": { 500 | "streamsearch": "^1.1.0" 501 | } 502 | }, 503 | "caniuse-lite": { 504 | "version": "1.0.30001431", 505 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", 506 | "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==" 507 | }, 508 | "client-only": { 509 | "version": "0.0.1", 510 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 511 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 512 | }, 513 | "glob-to-regexp": { 514 | "version": "0.4.1", 515 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 516 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 517 | }, 518 | "graceful-fs": { 519 | "version": "4.2.11", 520 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 521 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 522 | }, 523 | "js-tokens": { 524 | "version": "4.0.0", 525 | "peer": true 526 | }, 527 | "loose-envify": { 528 | "version": "1.4.0", 529 | "peer": true, 530 | "requires": { 531 | "js-tokens": "^3.0.0 || ^4.0.0" 532 | } 533 | }, 534 | "nanoid": { 535 | "version": "3.3.4", 536 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 537 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" 538 | }, 539 | "next": { 540 | "version": "13.5.1", 541 | "resolved": "https://registry.npmjs.org/next/-/next-13.5.1.tgz", 542 | "integrity": "sha512-GIudNR7ggGUZoIL79mSZcxbXK9f5pwAIPZxEM8+j2yLqv5RODg4TkmUlaKSYVqE1bPQueamXSqdC3j7axiTSEg==", 543 | "requires": { 544 | "@next/env": "13.5.1", 545 | "@next/swc-darwin-arm64": "13.5.1", 546 | "@next/swc-darwin-x64": "13.5.1", 547 | "@next/swc-linux-arm64-gnu": "13.5.1", 548 | "@next/swc-linux-arm64-musl": "13.5.1", 549 | "@next/swc-linux-x64-gnu": "13.5.1", 550 | "@next/swc-linux-x64-musl": "13.5.1", 551 | "@next/swc-win32-arm64-msvc": "13.5.1", 552 | "@next/swc-win32-ia32-msvc": "13.5.1", 553 | "@next/swc-win32-x64-msvc": "13.5.1", 554 | "@swc/helpers": "0.5.2", 555 | "busboy": "1.6.0", 556 | "caniuse-lite": "^1.0.30001406", 557 | "postcss": "8.4.14", 558 | "styled-jsx": "5.1.1", 559 | "watchpack": "2.4.0", 560 | "zod": "3.21.4" 561 | } 562 | }, 563 | "next-router-mock": { 564 | "version": "file:../..", 565 | "requires": { 566 | "@changesets/cli": "^2.26.2", 567 | "@testing-library/react": "^13.4.0", 568 | "@types/jest": "^26.0.20", 569 | "doctoc": "^2.2.0", 570 | "jest": "^26.6.3", 571 | "next": "^13.5.1", 572 | "prettier": "^2.2.1", 573 | "react": "^18.2.0", 574 | "react-dom": "^18.2.0", 575 | "react-test-renderer": "^18.2.0", 576 | "rimraf": "^3.0.2", 577 | "ts-jest": "^26.4.4", 578 | "typescript": "^4.9.5" 579 | } 580 | }, 581 | "picocolors": { 582 | "version": "1.0.0", 583 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 584 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 585 | }, 586 | "postcss": { 587 | "version": "8.4.14", 588 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 589 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 590 | "requires": { 591 | "nanoid": "^3.3.4", 592 | "picocolors": "^1.0.0", 593 | "source-map-js": "^1.0.2" 594 | } 595 | }, 596 | "react": { 597 | "version": "18.2.0", 598 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 599 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 600 | "peer": true, 601 | "requires": { 602 | "loose-envify": "^1.1.0" 603 | } 604 | }, 605 | "react-dom": { 606 | "version": "18.2.0", 607 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 608 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 609 | "peer": true, 610 | "requires": { 611 | "loose-envify": "^1.1.0", 612 | "scheduler": "^0.23.0" 613 | } 614 | }, 615 | "scheduler": { 616 | "version": "0.23.0", 617 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 618 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 619 | "peer": true, 620 | "requires": { 621 | "loose-envify": "^1.1.0" 622 | } 623 | }, 624 | "source-map-js": { 625 | "version": "1.0.2", 626 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 627 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 628 | }, 629 | "streamsearch": { 630 | "version": "1.1.0", 631 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 632 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" 633 | }, 634 | "styled-jsx": { 635 | "version": "5.1.1", 636 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 637 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 638 | "requires": { 639 | "client-only": "0.0.1" 640 | } 641 | }, 642 | "tslib": { 643 | "version": "2.6.2", 644 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 645 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 646 | }, 647 | "watchpack": { 648 | "version": "2.4.0", 649 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", 650 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", 651 | "requires": { 652 | "glob-to-regexp": "^0.4.1", 653 | "graceful-fs": "^4.1.2" 654 | } 655 | }, 656 | "zod": { 657 | "version": "3.21.4", 658 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", 659 | "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" 660 | } 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /test/next-13.5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "13.5.1", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/next-13/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the next version for these tests: 6 | "^next/(.*)$": "/node_modules/next/$1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/next-13/next-13.test.ts: -------------------------------------------------------------------------------- 1 | // Validate our types are exported correctly: 2 | import type { memoryRouter } from "next-router-mock"; 3 | import type { memoryRouter as ___ } from "next-router-mock/async"; 4 | import type { createDynamicRouteParser } from "next-router-mock/dynamic-routes"; 5 | import type { createDynamicRouteParser as _ } from "next-router-mock/dynamic-routes/next-13"; 6 | import type { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider"; 7 | import type { MemoryRouterProvider as __ } from "next-router-mock/MemoryRouterProvider/next-13"; 8 | 9 | describe(`next version ${require("next/package.json").version}`, () => { 10 | describe("automatic and explicit import paths are valid", () => { 11 | it("next-router-mock/dynamic-routes", () => { 12 | require("next-router-mock/dynamic-routes"); 13 | }); 14 | it("next-router-mock/dynamic-routes/next-13", () => { 15 | require("next-router-mock/dynamic-routes/next-13"); 16 | }); 17 | it("next-router-mock/MemoryRouterProvider", () => { 18 | require("next-router-mock/MemoryRouterProvider"); 19 | }); 20 | it("next-router-mock/MemoryRouterProvider/next-13", () => { 21 | require("next-router-mock/MemoryRouterProvider/next-13"); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/next-13/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "next": "13.0.2", 4 | "next-router-mock": "../.." 5 | }, 6 | "scripts": { 7 | "test": "tsc && jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-exports/exports.test.ts: -------------------------------------------------------------------------------- 1 | import router, { useRouter } from "next/router"; 2 | import { memoryRouter, MemoryRouter } from "../.."; 3 | import { createDynamicRouteParser } from "../../dynamic-routes"; 4 | import { MemoryRouterProvider } from "../../MemoryRouterProvider"; 5 | 6 | jest.mock("next/router", () => require("../..")); 7 | 8 | describe("exports", () => { 9 | it("next/router should be exported correctly", () => { 10 | expect(router).toBeInstanceOf(MemoryRouter); 11 | expect(router).toBe(memoryRouter); 12 | expect(useRouter).toBeInstanceOf(Function); 13 | }); 14 | describe("next-router-mock/dynamic-routes", () => { 15 | it("should be exported correctly", () => { 16 | expect(createDynamicRouteParser).toBeInstanceOf(Function); 17 | }); 18 | }); 19 | describe("next-router-mock/MemoryRouterProvider", () => { 20 | it("should be exported correctly", () => { 21 | expect(MemoryRouterProvider).toBeInstanceOf(Function); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/test-exports/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | }; 5 | -------------------------------------------------------------------------------- /test/test-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import type { MemoryRouter } from "../src/MemoryRouter"; 2 | 3 | /** 4 | * Performs a partial equality comparison. 5 | * 6 | * This is similar to using `toMatchObject`, but doesn't ignore missing `query: { ... }` values! 7 | */ 8 | export function expectMatch(memoryRouter: MemoryRouter, expected: Partial): void { 9 | const picked = pick(memoryRouter, Object.keys(expected) as Array); 10 | try { 11 | expect(picked).toEqual(expected); 12 | } catch (err: any) { 13 | // Ensure stack trace is accurate: 14 | Error.captureStackTrace(err, expectMatch); 15 | throw err; 16 | } 17 | } 18 | 19 | function pick(obj: T, keys: Array): T { 20 | const result = {} as T; 21 | keys.forEach((key) => { 22 | result[key] = obj[key]; 23 | }); 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "noEmit": false 7 | }, 8 | "exclude": [ 9 | "**/*.test.*", 10 | "test/**" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "NodeNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["ES2019", "DOM", "DOM.Iterable"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "exclude": ["test/example-app"] 71 | } 72 | --------------------------------------------------------------------------------