react
Set of helpers for testing React-components
Helpers are based on libraries @testing-library/react
If you are using jest for testing, consider to add a library @testing-library/jest-dom
react
should be installed manually
Installation
npm i --save-dev @tramvai/test-react
Only React >= 18 version is supported
Explanation
act
Based on the React act test helper that is used to perform rerender component after start changes.
Put you actions that will update React state inside act
function in order to check result render in the next code.
Current wrapper should be awaited in tests to execute some additional internal steps
How to
Test component
Under the hood the @testing-library/react is used.
/**
* @jest-environment jsdom
*/
import { testComponent } from '@tramvai/test-react';
describe('test', () => {
it('render', async () => {
const { screen, rerender, context, act, fireEvent, Wrapper } = testComponent(<Cmp id={1} />);
// test component render
expect(screen.getByTestId('test').textContent).toBe('Content 1');
// test render after store update
act(() => {
context.dispatch(event('data'));
});
// interact with the component
fireEvent.click(screen.getByText('Button'));
// component rerender
rerender(<Cmp id={2} />);
expect(screen.getByTestId('test').textContent).toBe('Content 2');
});
});
More examples
/**
* @jest-environment jsdom
*/
import React, { useEffect, useRef } from 'react';
import { createReducer, createEvent, useStore, useStoreSelector } from '@tramvai/state';
import { useDi } from '@tramvai/react';
import { useRoute } from '@tinkoff/router';
import { testComponent } from './testComponent';
describe('test/unit/react/testComponent', () => {
it('should render simple component', async () => {
const Cmp = () => {
return (
<div>
<div>Root</div>
<div data-testid="test">Content</div>
</div>
);
};
const { screen } = testComponent(<Cmp />);
expect(screen.getByTestId('test').textContent).toBe('Content');
});
it('should rerender component on store updates', async () => {
const event = createEvent<void>('evnt');
const store = createReducer('store', { a: 1 }).on(event, (state) => ({ a: state.a + 1 }));
const Cmp = () => {
const { a } = useStore(store);
return (
<div>
<span data-testid="content">Counter: {a}</span>
</div>
);
};
const { context, screen, act } = testComponent(<Cmp />, { stores: [store] });
expect(screen.getByTestId('content').textContent).toBe('Counter: 1');
await act(() => {
context.dispatch(event());
});
expect(screen.getByTestId('content').textContent).toBe('Counter: 2');
});
it('should rerender component on store updates using storeSelector', async () => {
const event = createEvent<void>('evnt');
const store = createReducer('store', { a: 1 }).on(event, (state) => ({ a: state.a + 1 }));
const Cmp = () => {
const a = useStoreSelector(store, (s) => s.a);
return (
<div>
<span data-testid="content">Counter: {a}</span>
</div>
);
};
const { context, screen, act } = testComponent(<Cmp />, { stores: [store] });
expect(screen.getByTestId('content').textContent).toBe('Counter: 1');
await act(() => {
context.dispatch(event());
});
expect(screen.getByTestId('content').textContent).toBe('Counter: 2');
});
it('should work with di', async () => {
const Cmp = () => {
const { provider } = useDi({ provider: 'provider' });
return <span role="status">{provider}</span>;
};
const { render } = testComponent(<Cmp />, {
providers: [
{
provide: 'provider',
useValue: 'test',
},
],
});
expect(render.getByRole('status')).toMatchInlineSnapshot(`
<span
role="status"
>
test
</span>
`);
});
it('should work with routing', async () => {
const Cmp = () => {
const route = useRoute();
return (
<div>
<div data-testid="route">
<div data-testid="route-path">{route.actualPath}</div>
<div data-testid="route-name">{route.name}</div>
</div>
</div>
);
};
const { render } = testComponent(<Cmp />, { currentRoute: { name: 'test', path: '/test/' } });
expect(render.getByTestId('route-path').textContent).toBe('/test/');
expect(render.getByTestId('route-name').textContent).toBe('test');
});
it('should rerender simple component', async () => {
expect.hasAssertions();
const Cmp = ({ label }: { label: string }) => {
const count = useRef(1);
useEffect(() => {
count.current += 1;
}, [label]);
return (
<div>
<div>Root</div>
<div data-testid="test">{`${count.current}. ${label}`}</div>
</div>
);
};
const { render, rerender } = testComponent(<Cmp label="first render" />);
expect(render.getByTestId('test').textContent).toBe('1. first render');
rerender(<Cmp label="second render" />);
expect(render.getByTestId('test').textContent).toBe('2. second render');
});
it('should rerender component on props updates', async () => {
const event = createEvent<void>('evnt');
const store = createReducer('store', { a: 1 }).on(event, (state) => ({ a: state.a + 1 }));
const Cmp = ({ label }: { label: string }) => {
const count = useRef(1);
const { a } = useStore(store);
useEffect(() => {
count.current += 1;
}, [label]);
return (
<div>
<span data-testid="content">{`${count.current}. ${label}: ${a}`}</span>
</div>
);
};
const { context, render, rerender, act } = testComponent(<Cmp label="first render" />, {
stores: [store],
});
expect(render.getByTestId('content').textContent).toBe('1. first render: 1');
await act(() => {
context.dispatch(event());
});
expect(render.getByTestId('content').textContent).toBe('2. first render: 2');
rerender(<Cmp label="second render" />);
expect(render.getByTestId('content').textContent).toBe('2. second render: 2');
});
});
Test React-hooks
Under the hood the @testing-library/react is used.
/**
* @jest-environment jsdom
*/
import { testHook } from '@tramvai/test-react';
describe('test', () => {
it('hook', async () => {
const { result, context, act } = testHook(() => useHook());
// test the result of hook call
expect(result.current).toBe('result');
// test the result after store update
act(() => {
context.dispatch(event('data'));
});
});
});
More examples
/**
* @jest-environment jsdom
*/
import { createReducer, createEvent, useStore } from '@tramvai/state';
import { useDi } from '@tramvai/react';
import { useRoute } from '@tinkoff/router';
import { waitRaf } from '@tramvai/test-jsdom';
import { testHook } from './testHook';
describe('test/unit/react/testHook', () => {
it('should render simple hook', async () => {
const useHook = jest.fn((p: string) => 'result');
const { result } = testHook(() => useHook('test'));
expect(result.current).toBe('result');
expect(useHook).toHaveBeenCalledWith('test');
});
it('should rerender hook', async () => {
const event = createEvent<void>('evnt');
const store = createReducer('store', { a: 1 }).on(event, (state) => ({ a: state.a + 1 }));
const useHook = () => {
return useStore(store).a;
};
const { context, result, act } = testHook(() => useHook(), { stores: [store] });
expect(result.current).toBe(1);
await act(async () => {
await context.dispatch(event());
await waitRaf();
});
expect(result.current).toBe(2);
});
it('should work with di', async () => {
const useHook = () => {
return useDi({ provider: 'provider' }).provider;
};
const { result } = testHook(() => useHook(), {
providers: [
{
provide: 'provider',
useValue: 'test',
},
],
});
expect(result.current).toEqual('test');
});
it('should work with routing', async () => {
const useHook = () => {
const route = useRoute();
return [route.actualPath, route.name];
};
const { result } = testHook(() => useHook(), {
currentRoute: { name: 'test', path: '/test/' },
});
expect(result.current).toEqual(['/test/', 'test']);
});
});
Troubleshooting
Warning: ReactDOM.render is no longer supported in React 18
@tramvai/test-react
comes with support for react 16 and 17 so if you are using react@18 it will lead to the above warning as this backward-compatibility forces to use legacy render methods.
You can manually specify not to use legacy rendering mode by settings option legacyRoot
to false
/**
* @jest-environment jsdom
*/
import { testComponent } from '@tramvai/test-react';
describe('test', () => {
it('component', async () => {
const { render } = testComponent(<Cmp id={1} />, { legacyRoot: false });
});
});