Why does a react component, tested with React Testing Library has issue with react-intersection-observer? - reactjs

I wrote a component built with react 17.0.2 that uses react-intersection-observer 9.1.0
import { useInView } from 'react-intersection-observer'
...
const [ref, inView] = useInView({
threshold: 0.99,
root: scrollRef.current,
delay: 250,
trackVisibility: true,
onChange: (inView: boolean) => {
onChildInView(index, inView)
}
})
to detect sliding behaviours inside or outside the viewport. And the component works fine.
I wrote some unit tests to make the component safer, using #testing-library/react 12.1.4 and #testing-library/jest-dom 5.16.3.
As soon as I test just the existence or visibility of the above component with the following code
describe('method render', () => {
test('renders correctly', () => {
render(
<MyComponent
props={...}
data-testid="component-id"
>
<div />
<div />
</MyComponent>
)
const componentNode = screen.getByTestId('component-id')
expect(componentNode).toBeInTheDocument()
expect(componentNode).toBeVisible()
})
})
the testing library complains with the message error.
ReferenceError: IntersectionObserver is not defined
I tried to fix it with this suggestion of mocking the library (as linked here) written at the top of the test
const intersectionObserverMock = () => ({
observe: () => null
})
declare global {
interface Window {
IntersectionObserver: typeof IntersectionObserver
}
}
window.IntersectionObserver = jest.fn().mockImplementation(intersectionObserverMock);
but it did not work due to
TypeError: observer.unobserve is not a function
Suggestions? Missing something?

To fix this issue I'd recommend using mockAllIsIntersecting from test-utils.js in react-intersection-observer. This function mocks the IntersectionObserver.
e.g.
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils';
describe('method render', () => {
test('renders correctly', () => {
render(
<MyComponent
props={...}
data-testid="component-id"
>
<div />
<div />
</MyComponent>
)
mockAllIsIntersecting(true)
const componentNode = screen.getByTestId('component-id')
expect(componentNode).toBeInTheDocument()
expect(componentNode).toBeVisible()
})
})

Related

How to properly mock named export children components with jest

So, here is a simplified version of both my component and my test.
export const VerifyPositionsDialog = () => {
return (
<BaseVerifyPositionsDialog>
<PositionsArea />
</BaseVerifyPositionsDialog>
);
};
I've omitted props and components logic for better readability.
What I've been trying to do is to mock PositionsArea component, so I can unit test VerifyPositionsDialog isolatedly.
This is my test so far.
jest.mock("../VerifyPositionsDialog/PositionsArea", () => ({
__esModule: true,
PositionsArea: () => <div />,
}));
render(<VerifyPositionsDialog />);
I've already tried a lot of different ways based on other answer from SO, but none seems to work.
Any help would be awesome.
You should mock the component using jest.fn returning the mocked div:
jest.mock('../VerifyPositionsDialog/PositionsArea', () =>
jest.fn(() => <div>Mocked</div>),
);
describe('Test', () => {
it('Mock Component Test', () => {
const { debug } = render(<VerifyPositionsDialog />);
// You will check with debug() that mocked div was rendered
debug();
});
});

Unable to find an element with React testing library testing a ternary operator

In my React component <MyBlock /> I have the following conditional rendering:
So when variant="one" prop is passed it will return <Container /> component, when variant="two" is passed it will render <Scroller />.
{variant === 'one' ? (
<Container items={items} testId="container" />
) : (
<Scroller items={items} testId="scroller" />
)}
With React testing library I am testing this component:
it('should render the container correctly', () => {
const { getByTestId } = mount(<MyBlock data={MockData} variant="one" />);
expect(getByTestId('container')).toBeDefined();
});
This test pass and works fine.
Now I want to test the other variant:
it('should render the container correctly', () => {
const { getByTestId } = mount(<MyBlock data={MockData} variant="two" />);
expect(getByTestId('scroller')).toBeDefined();
});
But then I get:
Unable to find an element by: [data-testid="scroller"]
What's the issue here?
I figured out. I have to use queryByTestId instead of getByTestId:
it('should render the container correctly', () => {
const { queryByTestId } = mount();
expect(queryByTestId('scroller')).toBeDefined();
});
Now it's working fine for both.

Test exception - unstable_flushDiscreteUpdates

I have a test that uses Jest and react-testing-library with a .tsx test file (TypeScript):
import React from "react";
import { Provider } from 'react-redux';
import { render, screen } from "#testing-library/react";
import Home from "./Home";
describe('Home', () => {
test("renders Programs without crashing", async () => {
const storeFake = (state: any) => ({
default: () => { },
subscribe: () => { },
dispatch: () => { },
getState: () => ({ ...state })
});
const store = storeFake({}) as any;
const { getByAltText } = render(<Provider store={store}>
<Home />
</Provider>);
// render should show the title
expect(getByAltText(/Great State/)).toBeInTheDocument();
});
The test passes but an exception is thrown:
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.
in video (at Home.tsx:10)
in div (at Home.tsx:9)
in Home (created by ConnectFunction)
in ConnectFunction (at Home.test.tsx:15)
in Provider (at Home.test.tsx:14)
First, does anyone know how to resovle the exception. Second, I know this is a warning but it seems pretty serious, shouldn't the test fail?
As the error message describes it's a React warning and not related with React Testing Library. It's actually a React issue according to this thread https://github.com/facebook/react/issues/10389 . You may encounter this warning when you're using a <video /> element with muted property.
You can try the below code within the beforeAll block or before the test cases are executed.
Object.defineProperty(HTMLMediaElement.prototype, "muted", {
set: jest.fn(),
});
It should work assuming that you're using <video /> tag inside your component like below.
<video src={videoPath} muted />
Property vs Attribute
This bug arises since React sets the muted property instead of correctly passing the attribute to the element: the first reflects the current value, while the latter reflects the initial state of the component. Setting muted as a prop is therefore incorrectly triggering an event.
Solution: you can also just set the 'muted' property in a useEffect hook.
function VideoComponent() {
const videoRef = createRef<HTMLVideoElement>()
const source = '/path/to.mp4'
useEffect(() => {
const { current: video } = videoRef
video && (video.muted = true)
}, [])
return (
<video
ref={videoRef}
src={source}
autoPlay loop playsInline
/>
)
}

Jest Invariant Violation

Simple use of react hooks in beta do not work when using jest and result in an error of
Invariant Violation: Hooks can only be called inside the body of a function component.
Despite much looking around I do not see working examples of Jest with react hooks. Yes I know it is beta but let us flag it now.
https://github.com/ghinks/jest-react-hook-issue.git
const MyDiv = () => {
const [count, setCount] = useState(0)
const clickHandler = () => { setCount(count + 1);
console.log(`clicked ${count}`) }
return (
<div onClick={clickHandler}>
ClickMe {count}
</div>
)
}
even simple tests
import { MyDiv } from './App';
describe('Test Component using hooks', () => {
test('MyDiv', () => {
const div = MyDiv();
expect(div).toMatchSnapshot();
})
});
will fail with the invariant error.
I would expect this to actually work.
looks like simple change to
describe('Test Component using hooks', () => {
test('MyDiv', () => {
expect(<MyDiv/>).toMatchSnapshot();
})
});
resolves this
The problem is what the error says. Function components aren't supposed to be called directly.
It should be as the reference shows:
import renderer from 'react-test-renderer';
...
const myDiv = renderer.create(<MyDiv/>).toJSON();
expect(myDiv).toMatchSnapshot();

How to test React components inside react-responsive tags

Inside a <MyComponent> component I am using react-responsive <MediaQuery> components to distinguish between rendering mobile and desktop content.
export class MyComponent extends React.Component {
//...
render() {
<MediaQuery query="(max-width: 600)">
<div className="inside-mobile">mobile view</div>
</MediaQuery>
}
}
I want to test the HTML inside <MyComponent>'s render() using enzyme, but can't seem to dive into the child elements of <MediaQuery>:
it('should dive into <MediaQuery>', () => {
const wrapper = mount(<Provider store={mockedStore}><MyComponent/></Provider>)
const actual = wrapper.getDOMNode().querySelectorAll(".inside-mobile")
expect(actual).to.have.length(1)
}
A console.log(wrapper.debug())shows that nothing inside <MediaQuery> is being rendered, though.
I'm guessing in a test (with no actual browser) window.width is not set which leads to the <MediaQuery> component not rendering anything.
What I want to do:
I want to be able to test <MyComponent>'s content using enzyme with react-responsive (or something similar such as react-media) to deal with mobile vs desktop viewports.
Things I've tried:
circumventing this by using enzyme's shallow with dive() instead of mount, to no avail.
using react-media's <Media> instead of react-responsive's <MediaQuery>, which seems to set window.matchMedia() to true by default. However, that's not working either.
console.log(wrapper.debug()) shows:
<MyComponent content={{...}}>
<Media query="(min-width: 600px)" defaultMatches={true} />
</MyComponent>
I found a working solution, using react-media instead of react-responsive, by mocking window.matchMedia so that matches is set to true during the test:
Create specific media components for different viewports:
const Mobile = ({ children, content }) => <Media query="(max-width: 600px)" children={children}>{matches => matches ? content : "" }</Media>;
const Desktop = ...
Use specific media component:
<MyComponent>
<Mobile content={
<div className="mobile">I'm mobile</div>
}/>
<Desktop content={...}/>
</MyComponent>
Test content per viewport:
const createMockMediaMatcher = matches => () => ({
matches,
addListener: () => {},
removeListener: () => {}
});
describe('MyComponent', () => {
beforeEach(() => {
window.matchMedia = createMockMediaMatcher(true);
});
it('should display the correct text on mobile', () => {
const wrapper = shallow(<MyComponent/>);
const mobileView = wrapper.find(Mobile).shallow().dive();
const actual = mobileView.find(".mobile").text();
expect(actual).to.equal("I'm mobile");
});
});

Resources