In my React app, I am using keycloak and the KeycloakProvider from the "react-keycloak/web" library.
Certain routes are protected using a PrivateRoute component that pulls the keycloak state using the useKeycloak() hook and checking the user is authorised and has the correct roles:
const [keycloak, initialized] = useKeycloak();
My issue comes with testing my PrivateRoute component. No matter what I try initialized always remains false, blocking the page from rendering.
Using the useKeycloak.test.js file as inspiration, I made a MockAuthProvider component to wrap around my tests:
import React from "react";
import { KeycloakProvider } from "#react-keycloak/web";
export const createKeycloakStub = () => ({
init: jest.fn().mockResolvedValue(true),
updateToken: jest.fn(),
login: jest.fn(),
logout: jest.fn(),
register: jest.fn(),
accountManagement: jest.fn(),
createLoginUrl: jest.fn(),
...
});
interface MockedAuthProviderProps {
children: React.ReactChild;
mocks: { [key: string]: typeof jest.fn };
}
export const MockedAuthProvider = (props: MockedAuthProviderProps) => {
const { children, mocks } = props;
const defaultMocks = createKeycloakStub();
const keycloak = { ...defaultMocks, ...mocks };
return (
<KeycloakProvider keycloak={keycloak}>
{children}
</KeycloakProvider>
);
};
I then use it in my tests as so
<MockedAuthProvider mocks={keycloakMocks}>
<MemoryRouter initialEntries={["/secret"]}>
<PrivateRoute roles={roles} path="/secret">
<div>Protected Content</div>
</PrivateRoute>
</MemoryRouter>
</MockedAuthProvider>
Has anyone found a way of successfully testing components in conjunction with useKeycloak or getting initialized to be true?
Thanks
I managed to work around it by mocking the hook itself. It's not quite as clean as I would have liked, but works well enough for now.
let mockInitialized = false;
jest.mock("#react-keycloak/web", () => {
const originalModule = jest.requireActual("#react-keycloak/web");
return {
...originalModule,
useKeycloak: () => [
mockKeycloakStub,
mockInitialized
]
};
})
I can then set the value of mockInitialized in each test, depending on what I want the initialized status to be.
Related
I'm using react-router V6 and trying to test the new feature of useOutletContext.
my testing library is testing-library/react and I'm not sure how to pass the Context data in the test.
In the TSX component, I'm getting the data with the hook of react-router:
const { data } = useOutletContext<IContext>()
I need something like:
test("render outlet context data view", async () => {
const { getByTestId } = render(
<MockedProvider mocks={[mockData]} context={myContextData}>
<ContextDataView />
</MockedProvider>
)
the MockedProvider tag is from #apollo/client/testing
the context={myContextData} part is what i need
Instead of mocking useOutletContext I used composition and React Router's MemoryRouter to mimic the behaviour of the real app.
I created a RenderRouteWithOutletContext component that should be used to wrap the component you're testing.
// RenderRouteWithOutletContext.tsx
import { ReactNode } from 'react';
import { MemoryRouter, Outlet, Route, Routes } from 'react-router-dom';
interface RenderRouteWithOutletContextProps<T = any> {
context: T;
children: ReactNode;
}
export const RenderRouteWithOutletContext = <T,>({
context,
children,
}: RenderRouteWithOutletContextProps<T>) => {
return (
<MemoryRouter>
<Routes>
<Route path="/"element={<Outlet context={context as T} />}>
<Route index element={children} />
</Route>
</Routes>
</MemoryRouter>
);
};
And in your test file:
// YourComponent.test.tsx
import { screen, cleanup, render } from '#testing-library/react';
import { describe, expect, it, afterEach } from 'vitest';
import { RenderRouteWithOutletContext } from './RenderRouteWithOutletContext';
const mockOutletContextData: any = {
foo: 'bar',
};
afterEach(() => {
cleanup();
});
describe('PersonOverview', () => {
it('should render as expected', () => {
render(
<RenderRouteWithOutletContext context={mockOutletContextData}>
<YourComponent />
</RenderRouteWithOutletContext>,
);
const component = screen.getByTestId('component-test-id');
expect(component).toBeInTheDocument();
});
});
Notice I'm using Vitest above but the Jest version of this is almost exactly the same.
This solution is great because it is very similar to how your app is actually used.
You can mock the useOutletContext hook like this:
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useOutletContext: () => myContextData,
})
);
I needed the same thing at work, and one of my colleagues helped me finally figure it out.
in your test file
import * as rrd from 'react-router-dom';
then set up your data just like you'd expect, and use Jest to mock React-router-dom
let mockData = { mock: 'Data' }
jest.mock('react-router-dom');
and then in your test
test("render outlet context data view", async () => {
rrd.useOutletContext.mockReturnValue(mockData)
render(<ContextDataView />)
}
I performed the following to mock one of the objects on my Outlet Context:
Outlet defined in Layout.tsx:
<Outlet context={{myModel1, myModel2, myModel3}} />
Test class:
import { act, cleanup, render, screen } from '#testing-library/react';
import { IMyModel1 } from 'Models/IMyModel1';
import * as rrd from 'react-router-dom';
jest.mock('react-router-dom');
const mockedOutletContext = rrd as jest.Mocked<typeof rrd>;
afterEach(() => {
cleanup;
mockedOutletContext.Outlet.mockReset();
});
Within your test, mock the object as required:
const myModel1: IMyModel1 = {};
const outletContext: rrd.OutletProps = { context: { myModel1: myModel1 } };
mockedOutletContext.useOutletContext.mockReturnValue(outletContext.context);
I found usefull informations on this Stack post : https://stackoverflow.com/questions/58117890/how-to-test-components-using-new-react-router-hooks/58206121#58206121
The right way to mock useOutletContext is to use the mock function like this :
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useOutletContext: () => ({
data: mockedData,
}),
}));
The mockedData is an object of data I'm using for the test.
At this point I had a small error TypeError: window.matchMedia is not a function. I found a solution in an other stack post (the solution is mentioned in jest documentation)
Here is the code to add to your test :
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
I am using React Testing Library to test some features of my website and I am facing issues with the initialization of the Redux store while running tests. The following is the initialState:
const initialState = {
isAuth: false,
loading: localStorage.getItem("authToken") ? true : false,
error: false,
user: {
username: "",
},
};
I have set up the tests with the following code to wrap everything around the Provider:
import { FC, ReactElement } from "react";
import { render, RenderOptions } from "#testing-library/react";
import { Provider } from "react-redux";
import { store } from "../store/store";
const AllTheProviders: FC = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from "#testing-library/react";
export { customRender as render };
Then, in the actual test I use beforeAll(() => window.localStorage.setItem("authToken", "mockToken") to set the token in localStorage. Based on the value of the loading state the Login component should be rendered on my website, but I'm always getting false as the value of loading.
import { render, screen, waitFor } from "../utils/test-utils";
import App from "../App";
beforeAll(() => window.localStorage.setItem("authToken", "MockAuthToken"));
test("Login page not rendered if a valid auth token is present", async () => {
render(<App />);
//this is to check that the Login component is not rendered
await waitFor(() => expect(screen.queryByText("Sign in")).toBeNull());
await waitFor(() => expect(screen.getByRole("navigation")).toBeDefined(), {
timeout: 5000,
});
});
Is this happening because the Redux store is created before the setItem function execution during the tests? While in the browser the token is already there when I enter the website and so the intended behavior is not resembled by the test.
That's correct. We have an open issue asking us to add a "lazy initialization function" overload for initialState in createSlice:
https://github.com/reduxjs/redux-toolkit/issues/1024
I was actually working on trying to implement this last night and have a PR up, but there's some concerns about changes in the semantics of behavior:
https://github.com/reduxjs/redux-toolkit/pull/1662
I'll look at this some more tonight and see what we can come up with.
I'm working on a unit test in a React application that verifies a passed in prop function is being conditionally called based on another props value. I'm utilizing Typescript/Enzyme/Jest in my application and am using a Root wrapper around the component I'm testing to inject the Redux store (and override initial state if desired).
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import Login from "src/components/auth/Login";
import Root from "src/Root";
let wrapped: ReactWrapper;
let defaultProps = {
signIn: jest.fn(),
token: null,
};
beforeEach(() => {
wrapped = mount(
<Root>
<Login {...defaultProps} />
</Root>
);
});
describe("on mount", () => {
describe("if no token is supplied to the props", () => {
it("will call the props.signIn() function", () => {
expect(defaultProps.signIn).toHaveBeenCalled();
});
});
});
When I run the test the toHaveBeenCalled() (as well as toBeCalled(), tried both) are not registering any calls. However, I have supplied a console.log statement that is getting triggered within the same conditional that the signIn() function lives.
import React from 'react';
import { AuthState, JWT } from "src/components/auth/types";
import { signIn } from "src/redux/auth";
interface Props {
signIn: () => Promise<void>;
token: null | JWT;
}
class Login extends React.Component<Props> {
/**
* Sign the user in on mount
*/
public componentDidMount(): void {
if (!this.props.token) {
console.log("GETTING HERE");
this.props.signIn();
}
}
public render(): JSX.Elemeent {
// ... More code
}
}
const mapStateToProps = (state: AuthState) => {
return {
token: state.auth.token;
};
};
export default connect(mapStateToProps, { signIn })(Login);
I've gone through several related posts/articles but all of the different configurations, such as traversing enzyme to get the direct prop or utilizing spyOn, have failed.
The only thing I can figure that's different is my wrapping of the Login component with Root, but considering I can see the console.log being triggered this seems like a complete shot in the dark.
Can anyone tell me what I'm doing wrong here?
You've to wait for component to mount, so:
it("will call the props.signIn() function", (done) => {
setImmediate(() => {
expect(defaultProps.signIn).toHaveBeenCalled();
done()
});
});
Ended up being me forgetting to place the override via mapDispatchToProps and mapStateToProps in the connect function. This was causing my passed in signIn function to be overridden by the signIn action imported in the file. Updating with ownProps and conditionally utilizing the passed in value fixes the issue:
const mapStateToProps = (state: AuthState, ownProps: Props) => {
return {
token: ownProps.token || state.auth.token;
};
};
const mapDispatchToProps = (dispatch: ThunkDispatch<{}, {}, any>, ownProps: Props) => {
return {
signIn: ownProps.signIn || (() => { return dispatch(signIn()) })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
I'm using Typescript. I want to
Create a context
Use the context in routing
Update the context when logged in.
I'm following this tutorial, except Typescript cut my hopes short. See below
I have this in my App.tsx
import React from 'react';
import './App.css';
import Login from "./auth/login";
import Home from "./dash/home";
import Header from "./menu";
const initialState = {
isAuthenticated: false,
user: null,
token: null,
};
export const AuthContext = React.createContext(); // <---- This right here complains:
// Expected 1-2 arguments, but got 0.ts(2554)
// index.d.ts(349, 9): An argument for 'defaultValue' was not provided.
const reducer = (state: any, action: any) => {
switch (action.type) {
case "LOGIN":
return {
...state,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token
};
case "LOGOUT":
localStorage.clear();
return {
...state,
isAuthenticated: false,
user: null
};
default:
return state;
}
};
const App: React.FC = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<AuthContext.Provider
value={{
state,
dispatch
}}
>
<Header />
<div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
</AuthContext.Provider>
);
}
export default App;
In my login page, I have this:
import React from "react";
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { AuthContext } from "../App";
export const Login = () => {
const { dispatch } = React.useContext(AuthContext) // <--- This the right way to do it?
// Where is the "dispatch" magically coming from?
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res);
})
}
return (
<div className="login-container">
<!-- assume there's a form here -->
</div>
);
};
export default Login;
So
What do I put in for the "defaultValue"?
How do I update the context after login?
EDIT:
More context (pun intended) of what I wanna achieve.
The App.tsx has an <AuthContext.Provider ...> ... </AuthContext.Provider> If I understand correctly, this Context takes in the value of the state, dispatch and as per the {!state.isAuthenticated ... } part, dynamically alternates between <Login /> and <Home/> component.
This <AuthContext.Provider/> as per the initialState is set to isAuthenticated: false
When the user logs in at the Login Component, my question is, how do I, via the AuthContext, update the values in the App.tsx so as the routing will change?
If you take a look at the docs about React.createContext
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them. Note: passing undefined as a Provider value does not cause consuming components to use defaultValue.
So you don't need to pass anything to the context, but you are getting the error because of typescript.
You need to create an interface for the context value.
You are passing the context as
value={{
state,
dispatch
}}
But you need to have an interface for that when create the context.
export const AuthContext = React.createContext(); // <---- missing interface
Here is an example on how to do it.
Where is the "dispatch" magically coming from?
This is how useContext works.
The value you get form useContext(MyContex) is the value passed by MyContext.Provider by value prop.
e.g.
<MyContext.Provider value={fooValue}>
{/* ... */}
</MyContext.Provider>
// same fooValue from the Provider
const fooValue = useState(MyContext)
Edit
After the async call inside onSubmit you must call dispatch and pass {type: 'LOGIN', payload: DATA_FROM_API} so it does in the reducer method and sets isAuthenticated to true
const onSubmit = (data: any) => {
axios.post('http://localhost/api/v1/users/login/', data)
.then(res => {
console.log(res.data) // <= if this is `payload`, than pass it to dispatch
dispatch({type: 'LOGIN', payload: res.data})
})
}
I had a similar problem, but after many trials and errors I was able to fix it.
I was following this tutorial:
Link: https://soshace.com/react-user-login-authentication-using-usecontext-and-usereducer/
Code: https://github.com/nero2009/login-auth-useContext
The problem about routing is also covered there. But it is in JS, so you have to make some modifications. Here is what I did:
First, I specified the action types, in your example it would be:
export type AuthenticationAction = { type: 'LOGIN', payload: IPayload } | {type: 'LOGOUT'}
I specified an interface for my payload, but it may work with "any" as well.
Then, I specified the default values (this is probably what you were looking for):
const AuthenticationStateContext = React.createContext(initialState)
const AuthenticationDispatchContext = React.createContext({} as Dispatch<AuthenticationAction>)
After that it should work.
I have the following component in my react redux project and I'm trying to implement tests but running into problems when trying to test the components
Users.js
export class Users extends Component {
componentWillMount() {
const dispatch = this.context.store.dispatch;
dispatch({ type: UserActions.fetchUsers(dispatch)});
}
render() {
const {users,isFetching} = this.props;
return (
<div>
<CardHeader
title={this.props.route.header}
style={{backgroundColor:'#F6F6F6'}}/>
<UserListHeader/>
{isFetching ? <LinearProgress mode="indeterminate" /> : <UserList users={users}/>}
</div>
)
}
}
Users.propTypes = {
users: PropTypes.array,
actions: PropTypes.object.isRequired
};
Users.contextTypes = {
store: React.PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
users: state.users.users,
isFetching:state.users.isFetching
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(UserActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Users);
and I'm trying to test it like the example on the Redux website, but I'm running into issues
UsersSpec.js
import {Users} from '/containers/users/Users'
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
function usersSetup() {
const props = {
users: expect.createSpy(),
isFetching:expect.createSpy()
};
const enzymeWrapper = mount(<Users />,{context:{store:mockStore}});
return {
props,
enzymeWrapper
}
}
describe('Users', () => {
it('should render self and subcomponents', () => {
const { enzymeWrapper } = usersSetup();
exp(enzymeWrapper.find(UserListHeader)).to.have.length(1);
})
})
But I get the error 'TypeError: dispatch is not a function' should I be mocking the componentWillMount function or how should I test this component.
Should I just be testing the dumb child components? Any guidance would be great. Thanks
mount function provides full dom rendering therefore you'll need to set up jsdom in your test setup. You can see more info here:
Error: It looks like you called `mount()` without a global document being loaded
Another thing is that, you should provide childContextTypes attribute when you're defining context with mount like this:
mount(<Users />, {
context: { store: mockStore },
childContextTypes: { store: PropTypes.object }
});
However if you're writing unit test of a React component you should use shallow function provided by enzyme. It just renders the component with one deep level, so that you don't need to create jsdom and provide context parameters when you're creating the wrapper component. mount rendering in a test means you're actually writing an integration test.
const wrapper = shallow(<Users />);
wrapper.find(UserListMenu).to.have.length(1);