Skip to content

Commit

Permalink
Fix greedy * in route paths
Browse files Browse the repository at this point in the history
This prevents `*` from matching without a preceding `/` in the URL
pathname. So e.g. `/users/*` matches `/users/mj` but not `/userstypo`.

This also fixes `to="."` in splat routes so they point to the route
path *including* the portion of the URL that was matched by the `*`,
which also makes `*` more consistent with `:param` since it's treated
just the same as any other param.

There is however a subtle breaking change if you are using the low-level
`match` API, e.g. the `match` object you get back from `matchRoutes()`
or `useMatch()`. `match.pathname` in a splat route now includes the full
URL pathname that was matched instead of only the portion before the
`*`. There is a new variable, `match.pathnameStart` that you can use if
you needed this for doing your own route matching.

Fixes #7972
  • Loading branch information
mjackson committed Sep 17, 2021
1 parent 9eebe8b commit e0933c8
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 238 deletions.
82 changes: 82 additions & 0 deletions packages/react-router/__tests__/greedy-matching-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from "react";
import { create as createTestRenderer } from "react-test-renderer";
import {
MemoryRouter as Router,
Routes,
Route,
Outlet
} from "react-router-dom";

describe("greedy matching", () => {
let routes = (
<Routes>
<Route path="/" element={<p>Root</p>} />
<Route
path="home"
element={
<div>
Home Layout <Outlet />
</div>
}
>
<Route index element={<p>Home</p>} />
<Route path="*" element={<p>Home Not Found</p>} />
</Route>
<Route path="*" element={<p>Not Found</p>} />
</Routes>
);

it("matches the root route", () => {
let renderer = createTestRenderer(
<Router initialEntries={["/"]} children={routes} />
);

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<p>
Root
</p>
`);
});

it("matches the index route", () => {
let renderer = createTestRenderer(
<Router initialEntries={["/home"]} children={routes} />
);

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
Home Layout
<p>
Home
</p>
</div>
`);
});

it('matches the nested "not found" route', () => {
let renderer = createTestRenderer(
<Router initialEntries={["/home/typo"]} children={routes} />
);

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
Home Layout
<p>
Home Not Found
</p>
</div>
`);
});

it('matches the "not found" route', () => {
let renderer = createTestRenderer(
<Router initialEntries={["/hometypo"]} children={routes} />
);

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<p>
Not Found
</p>
`);
});
});
132 changes: 132 additions & 0 deletions packages/react-router/__tests__/matchPath-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { matchPath } from "react-router";

describe("matchPath", () => {
it("matches the root / URL", () => {
let match = matchPath("/", "/")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/",
pathnameStart: "/"
});
});

it("matches a pathname", () => {
let match = matchPath("users", "/users")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/users",
pathnameStart: "/users"
});
});

it("matches a pathname with multiple segments", () => {
let match = matchPath("users/mj", "/users/mj")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/users/mj",
pathnameStart: "/users/mj"
});
});

it("matches a pathname with a trailing slash", () => {
let match = matchPath("/users", "/users/")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/users/",
pathnameStart: "/users/"
});
});

it("matches a pathname with multiple segments and a trailing slash", () => {
let match = matchPath("/users/mj", "/users/mj/")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/users/mj/",
pathnameStart: "/users/mj/"
});
});

describe("with a / pattern and { end: false }", () => {
it("matches a pathname", () => {
let match = matchPath({ path: "/", end: false }, "/users")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/",
pathnameStart: "/"
});
});

it("matches a pathname with multiple segments", () => {
let match = matchPath({ path: "/", end: false }, "/users/mj")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/",
pathnameStart: "/"
});
});

it("matches a pathname with a trailing slash", () => {
let match = matchPath({ path: "/", end: false }, "/users/")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/",
pathnameStart: "/"
});
});

it("matches a pathname with multiple segments and a trailing slash", () => {
let match = matchPath({ path: "/", end: false }, "/users/mj/")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/",
pathnameStart: "/"
});
});
});

it("is not case-sensitive by default", () => {
let match = matchPath({ path: "/SystemDashboard" }, "/systemdashboard")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/systemdashboard",
pathnameStart: "/systemdashboard"
});
});

it("matches a case-sensitive pathname", () => {
let match = matchPath(
{ path: "/SystemDashboard", caseSensitive: true },
"/SystemDashboard"
)!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
pathname: "/SystemDashboard",
pathnameStart: "/SystemDashboard"
});
});

it("does not match a case-sensitive pathname with the wrong case", () => {
let match = matchPath(
{ path: "/SystemDashboard", caseSensitive: true },
"/systemDashboard"
);
expect(match).toBeNull();
});

describe("when the pattern has a trailing *", () => {
it("matches the remaining portion of the pathname", () => {
let match = matchPath("/files/*", "/files/mj.jpg")!;
expect(match).not.toBeNull();
expect(match).toMatchObject({
params: { "*": "mj.jpg" },
pathname: "/files/mj.jpg",
pathnameStart: "/files"
});
});

it("matches only after a slash", () => {
let match = matchPath("/files/*", "/filestypo");
expect(match).toBeNull();
});
});
});
1 change: 1 addition & 0 deletions packages/react-router/__tests__/matchRoutes-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe("matchRoutes", () => {

it("matches root * routes correctly", () => {
expect(pickPaths(routes, "/not-found")).toEqual(["*"]);
expect(pickPaths(routes, "/hometypo")).toEqual(["*"]);
});

it("matches index routes correctly", () => {
Expand Down
86 changes: 0 additions & 86 deletions packages/react-router/__tests__/nested-params-test.tsx

This file was deleted.

Loading

0 comments on commit e0933c8

Please sign in to comment.