Example code:
import React, { FC } from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { createMemoryHistory } from 'history';
import { MemoryRouter, Router, Route, Switch, Link } from 'react-router-dom';
type TRoutes = {
[key: string]: {
title?: string;
path: string;
component: FC;
};
};
const routes: TRoutes = {
start: {
title: 'start page',
path: `/`,
component: () => null
},
destination: {
path: `/destination`,
component: () => null
}
};
const initialEntries = [routes.start.path];
const history = createMemoryHistory({
initialEntries
});
let wrapper: ReactWrapper;
describe('Router test', () => {
beforeAll(() => {
wrapper = mount(
<MemoryRouter initialEntries={initialEntries}>
<Router history={history}>
<Link to={routes.destination.path}>Go to destination</Link>
<button
type="button"
onClick={() => {
history.push(routes.destination.path);
}}
>
Go to route
</button>
<Switch>
<Route path={routes.start.path} exact component={routes.start.component} />
<Route path={routes.destination.path} exact component={routes.destination.component} />
</Switch>
</Router>
</MemoryRouter>
);
});
describe('Link', () => {
beforeAll(() => {
history.push(routes.start.path);
});
it(`should render Link - Go to destination`, () => {
expect(wrapper.find('a').at(0).exists).toBeTruthy();
});
it(`should update history from='${routes.start.path}' to location='${routes.destination.path}' on Link click`, () => {
wrapper
.find('a')
.at(0)
.simulate('click');
// wrapper.update();
expect(history.location.pathname).toEqual(routes.destination.path);
});
});
describe('button', () => {
beforeAll(() => {
history.push(routes.start.path);
});
it(`should render button - Go to destination`, () => {
expect(wrapper.find('button').at(0).exists).toBeTruthy();
});
it(`should update history from='${routes.start.path}' to location='${routes.destination.path}' on history.push`, () => {
wrapper
.find('button')
.at(0)
.simulate('click');
// wrapper.update();
expect(history.location.pathname).toEqual(routes.destination.path);
});
});
});
I have 4 tests above.
it should test that the link exists = pass
on click of link it should update the history = fail
it should test that the button exists = pass
on click of button it should update the history = pass
The history updates only when I do history.push, but not when clicking the Link.
From the documents on - react-router-dom, it implies that this approach is valid and should work:
https://reactrouter.com/web/guides/testing
the url bar), you can add a route that updates a variable in the test:// app.test.js
test("clicking filter links updates product query params", () => {
let history, location;
render(
<MemoryRouter initialEntries={["/my/initial/route"]}>
<App />
<Route
path="*"
render={({ history, location }) => {
history = history;
location = location;
return null;
}}
/>
</MemoryRouter>,
node
);
act(() => {
// example: click a <Link> to /products?id=1234
});
// assert about url
expect(location.pathname).toBe("/products");
const searchParams = new URLSearchParams(location.search);
expect(searchParams.has("id")).toBe(true);
expect(searchParams.get("id")).toEqual("1234");
});
Any advice on this?
thanks
Related
My component code is as below. Not an expert in Jest mocking. referred How to mock useHistory hook in jest? and mocked useHistory.push. But the mock function is not being hit. I would appreciate any suggestions
const ReviseAction = ({
plans,
template,
coveragePercentage,
territoryName,
existingTemplateId,
}) => {
const history = useHistory();
const handleRevise = () => {
history.push({
pathname: "/xxx",
state: {
plans: plans,
template: template,
coveragePercentage: coveragePercentage,
territoryName: territoryName,
existingTemplateId: existingTemplateId,
},
});
};
return (
<button
data-testid="revise-button"
onClick={handleRevise}
key="revise-button"
>
Revise
</button>
);
};
Here is my test:
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ReviseAction from "./ReviseAction";
import { HashRouter as Router } from "react-router-dom";
describe("ReviseAction", () => {
jest.mock("react-router-dom");
const pushMock = jest.fn();
//reactRouterDom.useHistory = jest.fn().mockReturnValue({push: pushMock});
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useHistory: () => ({
push: jest.fn()
})
}));
it("Renders component", async () => {
render(
<Router>
<ReviseAction
plans={[]}
template={{}}
coveragePercentage={"12"}
territoryName={"Name"}
existingTemplateId={"1234"}
/>
</Router>
);
fireEvent.click(screen.queryByTestId("revise-button"));
expect(pushMock).toHaveBeenCalled();
});
});
and getting
Expected number of calls: >= 1
Received number of calls: 0
This fixed it.
What I did:
Changed HashRouter import to router
passed the mock history as props to router.
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ReviseAction from "./ReviseAction";
import { Router } from "react-router-dom";
describe("ReviseAction", () => {
const mockPush = jest.fn();
jest.mock("react-router-dom", () => ({
useHistory: () => ({
push: mockPush,
}),
}));
const mockHistory = { push: mockPush, location: {}, listen: jest.fn() };
it("Renders component", async () => {
render(
<Router history={mockHistory}>
<ReviseAction
plans={[]}
template={{}}
coveragePercentage={"12"}
territoryName={"Name"}
existingTemplateId={"1234"}
/>
</Router>
);
fireEvent.click(screen.queryByTestId("revise-button"));
expect(mockPush).toHaveBeenCalled();
});
});
I'm trying to test the redirection page when the user clicks on the button (I don't want to user jest.mock()).
I created the wrapper according to test-react-library documentation:
import { FC, ReactElement, ReactNode } from "react";
import { render, RenderOptions } from "#testing-library/react";
import { BrowserRouter } from "react-router-dom";
import userEvent from "#testing-library/user-event";
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/" } = {},
options?: Omit<RenderOptions, "wrapper">
) => {
window.history.pushState({}, "Home Page", route);
return {
user: userEvent.setup(),
...render(ui, { wrapper: WrapperProviders, ...options })
};
};
export * from "#testing-library/react";
export { customRender as render };
export type RenderType = ReturnType<typeof customRender>;
HomePage.tsx:
import { useNavigate } from "react-router-dom";
export default function HomePage() {
const navigate = useNavigate();
const handleClick = () => navigate("/other");
return (
<>
<h3>HomePage</h3>
<button onClick={handleClick}>Redirect</button>
</>
);
}
Other.tsx:
export default function Other() {
return <h3>Other</h3>;
}
HomePage.test.tsx:
import { render, RenderType } from "./customRender";
import HomePage from "./HomePage";
import "#testing-library/jest-dom/extend-expect";
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
user.click(button);
expect(getByText(/other/i)).toBeInTheDocument();
});
});
When I run the test, it fails and the page is not updated in the dom.
Does jest-dom does not support the re-render of the page and update the DOM? Or this test out of scope of the testing-library ?
From the code I can see that you are just rendering the HomePage component and inside of that component you don't have any logic that renders a new component based on the route changes (I suppose that you have that logic on another component). That's why when you click on the button you are not seeing the Other component rendered.
In this case I would suggest you to only make the assertions you need on the window.location object. So after you simulate the click on the button you can do:
expect(window.location.pathname).toBe("/other");
I updated the custom render and add a history for it:
const WrapperProviders: FC<{ children: ReactNode }> = ({ children }) => {
return <BrowserRouter>{children}</BrowserRouter>;
};
const customRender = (
ui: ReactElement,
{ route = "/",
history = createMemoryHistory({initialEntries: [route]}),
} = {},
options?: Omit<RenderOptions, "wrapper">
) => {
return {
user: userEvent.setup(),
...render( <Router location={history.location} navigator={history}>
ui
</Router>
, { wrapper: WrapperProviders, ...options })
};
};
and the test now passes:
describe("HomePage", () => {
let wrapper: RenderType;
beforeEach(() => {
wrapper = render(<HomePage />, { route: "/" });
});
test.only("Should redirects to other page", async () => {
const { getByText, user, history } = wrapper;
expect(getByText(/homepage/i)).toBeInTheDocument();
const button = getByText(/redirect/i);
expect(button).toBeInTheDocument();
await user.click(button);
expect(history.location.pathname).toBe('/other');
});
I'm using RedwoodJS which uses React with React Testing Library under the hood. I'm struggling to test a component (and all page components which have this component in the tree) because of the useLocation() hook.
When using a useLocation() hook inside my component, I need to wrap my component under test with a Router that mocks the browser location history to prevent the error Error: Uncaught [TypeError: Cannot read property 'pathname' of undefined].
However when I do that, the Navigation component is no longer fully rendered, so I can't test it... any ideas?
Navigation.js
//import statements
const renderListItems = (pathname) => {
const NavigationItems = [{..},{..},{..}] // example
return NavigationItems.map((item) => {
const selected = pathname.indexOf(item.path) ? false : true
return (
<ListItem
button
key={item.text}
onClick={() => {
navigate(item.route)
}}
selected={selected}
>
<ListItemText primary={item.text} />
</ListItem>
)
})
}
const Navigation = () => {
const { pathname } = useLocation() // this is why I need to wrap the Navigation component in a router for testing; I'm trying to get the current pathname so that I can give a specific navigation item an active state.
return (
<List data-testid="navigation" disablePadding>
{renderListItems(pathname)}
</List>
)
}
export default Navigation
Navigation.test.js
import { screen } from '#redwoodjs/testing'
import { renderWithRouter } from 'src/utilities/testHelpers'
import Navigation from './Navigation'
describe('Navigation', () => {
it('renders successfully', () => {
expect(() => {
renderWithRouter(<Navigation />)
}).not.toThrow()
})
it('has a "Dashboard" navigation menu item', () => {
renderWithRouter(<Navigation />)
expect(
screen.getByRole('button', { text: /Dashboard/i })
).toBeInTheDocument()
})
})
testHelpers.js
This is needed to prevent useLocation() inside Navigation.js from breaking the test.
import { Router, Route } from '#redwoodjs/router'
import { createMemoryHistory } from 'history'
import { render } from '#redwoodjs/testing'
const history = createMemoryHistory()
export const renderWithRouter = (Component) =>
render(
<Router history={history}>
<Route component={Component} />
</Router>
)
Resulting error
Navigation › has a "Dashboard" navigation menu item
TestingLibraryElementError: Unable to find an accessible element with the role "button"
There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
<body>
<div />
</body>
You can mock useLocation to return the pathname you want. This can apply to any function
Simple
//Put within testing file
jest.mock("router-package", () => ({
...jest.requireActual("router-package"),
useLocation: () => ({
pathname: "customPath/To/Return"
})
}));
Detailed
You can create a helper function where you can pass the path(string) and it automatically mocks it for you as such
random.test.js
import { setUpPageRender } from 'src/utilities/testHelpers'
import Navigation from './Navigation'
describe('Navigation', () => {
//Where we set up our custom path for the describe
const render = setUpPageRender('/customPathForThisDescribe/Foo')
it('renders successfully', () => {
expect(() => {
render(<Navigation />)
}).not.toThrow()
})
})
testHelpers.js
//Mocked Functions
jest.mock('router-package', () => ({
__esModule: true,
...jest.requireActual('router-package'),
useLocation: jest.fn(),
}))
import { useLocation } from 'router-package'
export const setUpPageRender = (location) => {
useLocation.mockReturnValue(location)
beforeEach(() => {
jest.clearAllMocks()
})
return (component) => {
return render( <Router history={history}>
<Route component={Component} />
</Router>)
}
}
I started studying integration testing and I am struggling with mocking.
I have a signup form that onSubmit calls firebase.auth.createUserWithEmailAndPassword, after that it initializes a document with the returned user uid and initialize it with default values.
I managed to check if createCserWithEmailAndPassword is correctly called but when I try to check if the collection set has been called I receive as error:
expect(received).toHaveBeenCalled()
Matcher error: received value must be a mock or spy function
Received has value: undefined
I am struggling here.
Here is my test implementation:
import React from "react";
import "#testing-library/jest-dom";
import { ThemeProvider } from "styled-components";
import { defaultTheme } from "../../styles";
import { createMemoryHistory } from "history";
import { Router, Switch, Route } from "react-router-dom";
import { render, fireEvent, wait } from "#testing-library/react";
import { SignUp } from "../../sections";
import { User } from "../../types";
import { auth, firestore } from "../../lib/api/firebase";
jest.mock("../../lib/api/firebase", () => {
return {
auth: {
createUserWithEmailAndPassword: jest.fn(() => {
return {
user: {
uid: "fakeuid",
},
};
}),
signOut: jest.fn(),
},
firestore: {
collection: jest.fn(() => ({
doc: jest.fn(() => ({
collection: jest.fn(() => ({
add: jest.fn(),
})),
set: jest.fn(),
})),
})),
},
};
});
const defaultUser: User = {
isActive: false,
accountName: "No balance",
startingBalance: 0.0,
monthlyBudget: 0.0,
};
const history = createMemoryHistory({ initialEntries: ["/signup"] });
const renderWithRouter = () =>
render(
<ThemeProvider theme={defaultTheme}>
<Router history={history}>
<Switch>
<Route exact path="/signup">
<SignUp />
</Route>
<Route exact path="/">
<div data-testid="GenericComponent"></div>
</Route>
</Switch>
</Router>
</ThemeProvider>
);
describe("<SignUp/>", () => {
it("correctly renders the signup form", () => {
const { getByTestId } = renderWithRouter();
const form = getByTestId("SignupForm");
expect(form).toBeInTheDocument();
});
it("correctly renders the signup form", () => {
const { getByTestId } = renderWithRouter();
const form = getByTestId("SignupForm");
expect(form).toBeInTheDocument();
});
it("let user signup with valid credentials", async () => {
const { getByPlaceholderText, getByTestId } = renderWithRouter();
const emailField = getByPlaceholderText("yourname#company.com");
const passwordField = getByPlaceholderText("Password");
const confirmPasswordField = getByPlaceholderText("Confirm Password");
const submitButton = getByTestId("Button");
fireEvent.change(emailField, {
target: { value: "test#test.com" },
});
fireEvent.change(passwordField, { target: { value: "Lampone01!" } });
fireEvent.change(confirmPasswordField, {
target: { value: "Lampone01!" },
});
fireEvent.click(submitButton);
expect(submitButton).not.toBeDisabled();
await wait(() => {
expect(auth.createUserWithEmailAndPassword).toHaveBeenCalled();
expect(
firestore.collection("users").doc("fakeUid").set(defaultUser)
).toHaveBeenCalled();
expect(history.location.pathname).toBe("/dashboard");
});
});
});
What am I doing wrong?
Thanks guys.
I have header component like below:
import { useLocation } from "react-router-dom";
const Header = () => {
let route = useLocation().pathname;
return route === "/user" ? <ComponentA /> : <ComponentB />;
}
How will you mock this useLocation() to get the path as user?
I cant simply call the Header component as below in my test file as I am getting an error:
TypeError: Cannot read property 'location' of undefined at useLocation
describe("<Header/>", () => {
it("call the header component", () => {
const wrapper = shallow(<Header />);
expect(wrapper.find(ComponentA)).toHaveLength(1);
});
});
I have tried looking similar to the link How to test components using new react router hooks? but it didnt work.
I have tried like below:
const wrapper = shallow(
<Provider store={store}>
<MemoryRouter initialEntries={['/abc']}>
<Switch>
<AppRouter />
</Switch>
</MemoryRouter>
</Provider>,
);
jestExpect(wrapper.find(AppRouter)
.dive()
.find(Route)
.filter({path: '/abc'})
.renderProp('render', { history: mockedHistory})
.find(ContainerABC)
).toHaveLength(1);
from the link Testing react-router with Shallow rendering but it didnt work.
Please let me know.
Thanks in advance.
I found that I can mock the React Router hooks like useLocation using the following pattern:
import React from "react"
import ExampleComponent from "./ExampleComponent"
import { shallow } from "enzyme"
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useLocation: () => ({
pathname: "localhost:3000/example/path"
})
}));
describe("<ExampleComponent />", () => {
it("should render ExampleComponent", () => {
shallow(<ExampleComponent/>);
});
});
If you have a call to useLocation in your ExampleComponent the above pattern should allow you to shallow render the component in an Enzyme / Jest test without error.
I've been struggling with this recently too...
I found this works quite nicely:
import React from "react"
import ExampleComponent from "./ExampleComponent"
import { shallow } from "enzyme"
const mockUseLocationValue = {
pathname: "/testroute",
search: '',
hash: '',
state: null
}
jest.mock('react-router', () => ({
...jest.requireActual("react-router") as {},
useLocation: jest.fn().mockImplementation(() => {
return mockUseLocationValue;
})
}));
describe("<ExampleComponent />", () => {
it("should render ExampleComponent", () => {
mockUseLocationValue.pathname = "test specific path";
shallow(<ExampleComponent/>);
...
expect(...
});
});
this way, I was able to both mock useLocation and provide a value for pathname in specific tests as necessary.
HTH
If you are using react-testing-library:
import React from 'react';
import { Router } from 'react-router-dom';
import { render } from '#testing-library/react';
import { createMemoryHistory } from 'history';
import Component from '../Component.jsx';
test('<Component> renders without crashing', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<Component />
</Router>
);
});
More info: https://testing-library.com/docs/example-react-router/
I know this isn’t a direct answer to your question, but if what you want is to test the browser location or history, you can use mount and add an extra Route at the end where you can “capture” the history and location objects.
test(`Foobar`, () => {
let testHistory
let testLocation
const wrapper = mount(
<MemoryRouter initialEntries={[`/`]}>
<MyRoutes />
<Route
path={`*`}
render={routeProps => {
testHistory = routeProps.history
testLocation = routeProps.location
return null
}}/>
</MemoryRouter>
)
// Manipulate wrapper
expect(testHistory)...
expect(testLocation)...
)}
Have you tried:
describe("<Header/>", () => {
it("call the header component", () => {
const wrapper = shallow(<MemoryRouter initialEntries={['/abc']}><Header /></MemoryRouter>);
expect(wrapper.find(Header).dive().find(ComponentA)).toHaveLength(1);
});
});
When you use shallow only the first lvl is rendered, so you need to use dive to render another component.
None of the solutions above worked for my use case(unit testing a custom hook). I had to override the inner properties of useLocation which was read-only.
\\ foo.ts
export const useFoo = () => {
const {pathname} = useLocation();
\\ other logic
return ({
\\ returns whatever thing here
});
}
/*----------------------------------*/
\\ foo.test.ts
\\ other imports here
import * as ReactRouter from 'react-router';
Object.defineProperty(ReactRouter, 'useLocation', {
value: jest.fn(),
configurable: true,
writable: true,
});
describe("useFoo", () => {
it(' should do stgh that involves calling useLocation', () => {
const mockLocation = {
pathname: '/path',
state: {},
key: '',
search: '',
hash: ''
};
const useLocationSpy = jest.spyOn(ReactRouter, 'useLocation').mockReturnValue(mockLocation)
const {result} = renderHook(() => useFoo());
expect(useLocationSpy).toHaveBeenCalled();
});
});