diff --git a/packages/react-router-dom/modules/Link.js b/packages/react-router-dom/modules/Link.js index 25e9de5411..df158ff028 100644 --- a/packages/react-router-dom/modules/Link.js +++ b/packages/react-router-dom/modules/Link.js @@ -1,5 +1,6 @@ import React from "react"; import { __RouterContext as RouterContext } from "react-router"; +import { createPath } from 'history'; import PropTypes from "prop-types"; import invariant from "tiny-invariant"; import { @@ -100,7 +101,8 @@ const Link = forwardRef( href, navigate() { const location = resolveToLocation(to, context.location); - const method = replace ? history.replace : history.push; + const isDuplicateNavigation = createPath(context.location) === createPath(normalizeToLocation(location)); + const method = (replace || isDuplicateNavigation) ? history.replace : history.push; method(location); } diff --git a/packages/react-router-dom/modules/__tests__/Link-click-test.js b/packages/react-router-dom/modules/__tests__/Link-click-test.js index e33b7d6eec..6be55188d4 100644 --- a/packages/react-router-dom/modules/__tests__/Link-click-test.js +++ b/packages/react-router-dom/modules/__tests__/Link-click-test.js @@ -9,15 +9,19 @@ import renderStrict from "./utils/renderStrict.js"; describe(" click events", () => { const node = document.createElement("div"); - afterEach(() => { - ReactDOM.unmountComponentAtNode(node); + let memoryHistory, pushSpy, replaceSpy; + + beforeEach(() => { + memoryHistory = createMemoryHistory(); + pushSpy = jest.spyOn(memoryHistory, "push"); + replaceSpy = jest.spyOn(memoryHistory, "push"); }); - const memoryHistory = createMemoryHistory(); - memoryHistory.push = jest.fn(); + afterEach(() => { + ReactDOM.unmountComponentAtNode(node); - beforeEach(() => { - memoryHistory.push.mockReset(); + pushSpy.mockRestore(); + replaceSpy.mockRestore(); }); it("calls onClick eventhandler and history.push", () => { @@ -40,15 +44,45 @@ describe(" click events", () => { }); expect(clickHandler).toBeCalledTimes(1); - expect(memoryHistory.push).toBeCalledTimes(1); - expect(memoryHistory.push).toBeCalledWith(to); + expect(pushSpy).toBeCalledTimes(1); + expect(pushSpy).toBeCalledWith(to); }); - it("calls onClick eventhandler and history.push with function `to` prop", () => { - const memoryHistoryFoo = createMemoryHistory({ - initialEntries: ["/foo"] + it("calls history.replace on duplicate navigation", () => { + const clickHandler = jest.fn(); + const to = "/duplicate/path?the=query#the-hash"; + + renderStrict( + + + link + + , + node + ); + + const a = node.querySelector("a"); + TestUtils.Simulate.click(a, { + defaultPrevented: false, + button: 0 }); - memoryHistoryFoo.push = jest.fn(); + + TestUtils.Simulate.click(a, { + defaultPrevented: false, + button: 0 + }); + + expect(clickHandler).toBeCalledTimes(2); + expect(pushSpy).toBeCalledTimes(1); + expect(pushSpy).toBeCalledWith(to); + expect(replaceSpy).toBeCalledTimes(1); + expect(replaceSpy).toBeCalledWith(to); + }); + + it("calls onClick eventhandler and history.push with function `to` prop", () => { + // Make push a no-op so key IDs do not change + pushSpy.mockImplementation(); + const clickHandler = jest.fn(); let to = null; const toFn = location => { @@ -61,7 +95,7 @@ describe(" click events", () => { }; renderStrict( - + link @@ -76,8 +110,8 @@ describe(" click events", () => { }); expect(clickHandler).toBeCalledTimes(1); - expect(memoryHistoryFoo.push).toBeCalledTimes(1); - expect(memoryHistoryFoo.push).toBeCalledWith(to); + expect(pushSpy).toBeCalledTimes(1); + expect(pushSpy).toBeCalledWith(to); }); it("does not call history.push on right click", () => { @@ -96,7 +130,7 @@ describe(" click events", () => { button: 1 }); - expect(memoryHistory.push).toBeCalledTimes(0); + expect(pushSpy).toBeCalledTimes(0); }); it("does not call history.push on prevented event.", () => { @@ -115,7 +149,7 @@ describe(" click events", () => { button: 0 }); - expect(memoryHistory.push).toBeCalledTimes(0); + expect(pushSpy).toBeCalledTimes(0); }); it("does not call history.push target not specifying 'self'", () => { @@ -136,12 +170,10 @@ describe(" click events", () => { button: 0 }); - expect(memoryHistory.push).toBeCalledTimes(0); + expect(pushSpy).toBeCalledTimes(0); }); it("prevents the default event handler if an error occurs", () => { - const memoryHistory = createMemoryHistory(); - memoryHistory.push = jest.fn(); const error = new Error(); const clickHandler = () => { throw error; @@ -173,6 +205,6 @@ describe(" click events", () => { console.error.mockRestore(); expect(clickHandler).toThrow(error); expect(mockPreventDefault).toHaveBeenCalled(); - expect(memoryHistory.push).toBeCalledTimes(0); + expect(pushSpy).toBeCalledTimes(0); }); });