When I add this line to the test setup, the mocking of useParams stops to work. I cannot find why it happens.
testsetup.ts
import '#testing-library/jest-dom'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3
}
}
})
// Clean up the react queries cache for each test.
afterEach(() => queryClient.clear())
component.test.tsx
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as any),
useParams: () => ({
tenantAlias: 'DE'
})
}))
test('Showing a navigation', async () => {
... some tests
})
Tested component is using useParams() to get params['tenantAlias'], but with queryClient.clear() it stops to returned mocked data, otherwise it works correct.
Why??? :)
Related
I use ReactJs, jest and react testing library. I have this code:
const App = ({data}) => {
const [state, setState] = useState(); // after useEffect runs state should be false
console.log('test state', state)
useEffect(() => {
setState(!data)
}, [])
return <p>'data is' {data},<p/>
}
<App data={12} />
After the useEffect will run the state should be false. Now i want to test the component using jest and react testing library.
describe('App', () => {
beforeEach(() => {
const setValue = jest.fn(x => {});
React.useState = jest.fn()
.mockImplementationOnce(x => [x, setValue])
})
test('It should render the correct value', async() => {
const v = utils.queryByText(12)
expect(v).toBeInTheDocument()
})
})
})
When i run the test in console.log('test state', state) i get for first time console.log('test state', undefined) and after that console.log('test state', false), but i need to get only the false value, because due the fact that for the first time the value is undefined the test fails. How to do this?
You need to wait until the component did mount and then run your expectation. In react testing library, there is a special method for it. waitFor.
from React testing library documentation:
When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass.
import {waitFor} from '#testing-library/react'
describe('App', () => {
// rest of the codes ...
test('It should render the correct value', async() => {
const v = utils.queryByText(12)
await waitFor(() => {
expect(v).toBeInTheDocument()
})
})
})
})
You are getting two console logs because this line initialises your state (undefined):
const [state, setState] = useState();
After that the useEffect hook runs onComponentDidMount and changes the state, which causes the component to rerender (second log statement).
You could use something like this, if you want to init the state from the props:
const [state, setState] = useState(!data);
Update:
This works on my machine:
Maybe you have some typos?
I am importing react-quill dynamically on the client side only using ssr: false. My functional component which is working fine, I want to add the quill-blot-formatter package to the modules part of my quill component.
My first roadblock is, I can't register this quill-blot-formatter with Quill as it shows:
ServerError
ReferenceError: document is not defined
This page is client rendered, therefore I don't understand where this error is coming from!
This is my code:
import dynamic from "next/dynamic";
import BlotFormatter from "quill-blot-formatter";
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
ssr: false,
loading: () => <p>Loading...</p>,
})
Quill.register("modules/blotFormatter", BlotFormatter);
Here, I don't understand how to bring Quill out of react-quill now that it's being imported dynamically. Therefore, I think that Quill.register isn't working. Now, how do I register quill-blot-formatter with react-quill? Following the Next.js example with react-quill, I am not even importing react-quill as ReactQuill as is the default export in the package.
Then I declared the blotFormatter module like this.
const modules = {
blotFormatter: {}, // here
toolbar: [
[{header: '1'}, {header: '2'}, {font: []}],
[{size: []}],
...
],
}
const formats = ['header','font','size','bold','italic','underline',...]
And used in the render() method like this:
export default function NewContent() {
...
render(
<QuillNoSSRWrapper
className={styles.quillTextArea}
id="quilleditor"
modules={modules}
formats={formats}
theme="snow"
onChange={handleTextChange}
readOnly={false}
/>
);
}
So far, this QuillNoSSRWrapper child component is doing it's job fine, but, how do I use the quill-blot-formatter in it's formats?
UPDATE
You don't need another useEffect to register module you can do it when you importing ReactQuill.
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
const { default: BlotFormatter } = await import("quill-blot-formatter");
RQ.Quill.register("modules/blotFormatter", BlotFormatter);
return function forwardRef({ forwardedRef, ...props }) {
return <RQ ref={forwardedRef} {...props} />;
};
},
{
ssr: false,
}
);
With this code you importing ReactQuill, register you module and pass ref that you can use later see details below. So with this code you now don't need any state. Additionally you can add custom loading function to dynamic details here.
After one day of searching i found the solution. First of all you import dynamic ReactQuill.
import dynamic from 'react/dynamic'
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
Or if you want to pass ref like this
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
// eslint-disable-next-line react/display-name
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
},
{
ssr: false,
}
);
And then you can use ref like this <ReactQuill ... forwardedRef={quillRef}/>
So then after you imported ReactQuill on client side you need to register module i found this solution here it's looks strange and i had no time to improve it but it's work. Here the code.
const loadQuill = async () => {
return new Promise(async (resolve, reject) => {
const Quill = await require("react-quill").Quill;
const BlotFormatter = (await import("quill-blot-formatter")).default;
resolve({ Quill, BlotFormatter });
})
.then(({ Quill, BlotFormatter }) => {
Quill.register("modules/blotFormatter", BlotFormatter);
return;
})
.then((value) => {
setEnableEditor(true);
});
};
useEffect(() => {
loadQuill();
}, []);
This code will execute on client side end register module. Also as you can see you need to declarate state enableEditor. And render ReactQuill only when enableEditor is true
{enableEditor && <ReactQuill ... />}
Looks bit wierd so maybe I will update it later
The app
The app is a simple To Do List. This App gets the todos from https://jsonplaceholder.typicode.com/todos?&_limit=5.
What I am trying to do
Test an API call that is executed in ComponentDidMount in App.tsx.
I want to mock the API call and return a list with two items. Then check if there are two items in the array or state.
What files are important for you?
App.tsx (Component to be tested)
ToDoList.test.tsx (Contains test function)
Small part of App.tsx to simplify it
class App extends Component {
public state: IState = {
items: [],
value : 5,
deleteItemParent : this.deleteItemParent
};
getAllTodos = async () => {
await fetch(
`https://jsonplaceholder.typicode.com/todos?&_limit=${this.state.value}`
)
.then((response) => response.json())
.then((json) => {
this.setState({ items: json })
});
};
componentDidMount() {
this.getAllTodos();
}
componentDidUpdate(prevProps : any, prevState: any) {
// Updates todo's if state changed
if (prevState.value !== this.state.value) {
this.getAllTodos();
}
}
render(){
return (
<div className="App">
<AddToDo addToDoParent={this.addToDo}/>
<div>
Todo's : {this.state.value}
</div>
<ToDoList items={this.state.items} deleteFromParent={this.deleteItemParent}/>
</div>
);
}
}
ToDoListMock.test.tsx
import React from 'react';
import { shallow , mount, ShallowWrapper} from 'enzyme';
import App from "../App";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(
[
{
title: "Todo1",
completed: false
},
{
title: "Todo2",
completed: false
}
]
)
})
) as jest.Mock<any>
it("must contain 2 items in state after mock api call", async () => {
const wrapper = shallow(<App />);
await new Promise(res => setTimeout(res));
// await jest.requireActual('promise').resolve()
// Collect items from state
const itemsFromState : any = wrapper.state('items');
// Expect it to have a length of 2
expect(itemsFromState.length).toBe(2);
})
The Error
Expected: 2
Received: 0
EDIT
SetupTests.ts
/* eslint-disable import/no-extraneous-dependencies */
import Enzyme from 'enzyme';
import ReactSixteenAdapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new ReactSixteenAdapter() });
I noticed an error while doing the test.
There are a few things you have to change to make it work:
You forgot to trigger to fetch your notes as your component mounted by putting to componentDidMount:
componentDidMount() {
this.getAllTodos();
}
Like you said, the data response is the list which is not a literal object, so you have to change your mock returning an array instead of literal object with todos property. But to make sure it runs well, we have to move beforeEach:
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([ // Returns as an array here
{
title: "Todo1",
completed : false
},
{
title: "Todo2",
completed : false
}
])
})
) as jest.Mock<any>
})
Delay your test before getting the state since you have a mock promise need to be done:
it("must contain 2 items in state after mock api call", async () => {
const wrapper = shallow(<App />);
// Delay with either following ways to wait your promise resolved
await new Promise(res => setTimeout(res));
// await jest.requireActual('promise').resolve()
// Collect items from state
const itemsFromState: Array<any> = wrapper.state('items');
// Expect it to have a length of 2
expect(itemsFromState.length).toBe(2);
})
I also created a link for you to compare your code to: https://repl.it/#tmhao2005/Todo
I'm building a login component using vue, vuex and vuetify. I've decided to use a namespaced auth module in store and this is causing me the problem.
I'm approaching this using TDD. My e2e test works. But I wrote this unit test (using a mockStore) that should only verify that a proper action has been dispatched:
describe('Login component', () => {
let wrapper
const mockStore = {
dispatch: jest.fn(),
}
beforeEach(() => {
wrapper = mount(Login, {
localVue,
mocks: { $store: mockStore },
computed: {
error: () => 'test error',
},
data: () => ({
valid: true
})
})
})
it('should dispatch login action', async () => {
wrapper.find('[data-test="username"]').setValue('username')
wrapper.find('[data-test="password"]').setValue('password')
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
wrapper.find('[data-test="login"]').trigger('click')
await wrapper.vm.$nextTick()
expect(mockStore.dispatch).toHaveBeenCalledWith(
`auth/${LOGIN}`,
{ username: 'username', password: 'password' }
)
})
})
The component is only using mapActions in the following way:
...mapActions('auth', [LOGIN])
And the button triggering it:
<v-btn
color="info"
#click="login({ username, password })"
data-test="login"
:disabled="!valid"
>Login</v-btn>
The error I'm getting is:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'auth/' of undefined"
If I drop the namespace in mapActions, then the dispatched action name I'm getting is not namespaced (duh) and the test fails:
- Expected
+ Received
- "auth/login",
+ "login",
I was actually able to fix it by mapping actions like so:
...mapActions({ login: `auth/${LOGIN}` })
But I would really prefer to use namespaced version, because it's gonna get ugly when I have more actions.
I was looking into vuex source code and it fails when trying to access _modulesNamespaceMap but then it gets too complicated for me.
What's the best approach to test this? Should I just give up on mocking and use a real store at this point?
Full project available here and commit relevant to this question is 4a7e749d4
Building off of the example on the vue-test-utils docs, I think this should work:
/* ... other imports and setup ... */
import Vuex from 'vuex'
describe('Login component', () => {
let wrapper
const actions = {
login: jest.fn(),
}
const mockStore = new Vuex({
modules: {
auth: {
namespaced: true,
actions,
},
},
})
beforeEach(() => {
wrapper = mount(Login, {
localVue,
mocks: { $store: mockStore },
computed: {
error: () => 'test error',
},
data: () => ({
valid: true
})
})
})
it('should dispatch login action', async () => {
wrapper.find('[data-test="username"]').setValue('username')
wrapper.find('[data-test="password"]').setValue('password')
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
wrapper.find('[data-test="login"]').trigger('click')
await wrapper.vm.$nextTick()
expect(actions.login).toHaveBeenCalled() // <-- pretty sure this will work
expect(actions.login).toHaveBeenCalledWith({ // <-- not as sure about this one
username: 'username',
password: 'password',
})
})
})
For anybody moving to Vue 3 Test Utils, please note that the createLocalVue method that the above answer relies upon is removed in #vue/test-utils (see https://next.vue-test-utils.vuejs.org/migration/#no-more-createlocalvue).
Instead, it recommends using the createStore method from Vuex. I was able to get namespaced modules working like this:
/* ... other imports and setup ... */
import { mount } from "#vue/test-utils";
import Logon from "path/to/your/logon/component";
import { createStore } from "vuex";
describe('Login component', () => {
const actions = {
login: jest.fn(),
};
const mockStore = createStore({
modules: {
auth: {
namespaced: true,
actions,
},
},
});
let wrapper;
beforeEach(() => {
wrapper = mount(Login, {
global: {
plugins: [mockStore],
},
});
});
it('should dispatch login action', async () => {
/*...test code goes here */
})
})
I'm using react-navigation and calling that service in my code. Although I'm not sure how to mock the navigate function. Here is my code:
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
Here is what I got so far:
export const loginEpic = (action$, state$, { ajax, navigate }) =>
action$.pipe(
ofType(LOGIN_REQUEST),
map(() => state$.value),
switchMap((options) =>
ajax(options).pipe(
pluck("response"),
map((res) => loginSuccess(res)),
tap((r) => navigate(ROUTES.DASHBOARD_SCREEN))
)
)
);
navigate is navigationService.navigate and I'm passing it from the dependencies of redux-observables.
The test looks like this:
const dependencies = {
ajax: ({ }) => of(mockedResponseFromAjax),
navigate: () => // ???
};
const result$ = loginEpic(action$, state$, dependencies).pipe(toArray());
Option 1: - What worked for me:
import NavigationService from '<your-path>/NavigationService';
beforeEach(() => {
NavigationService.navigate = jest.fn();
});
within my test file.
Option 2: - Better Version
see https://stackoverflow.com/questions/55319581/why-isnĀ“t-mock-from-mock-folder-invoked-when-running-test-for-redux-action
it is important that you activate the mock by jest.mock('../../app/utils/NavigationService'); this has to be placed at the top of your test file directly behind the imports