I'm using a plugin that renders out a form using json schema. For elements like input, button, etc, it uses a React component within the structure to render out the component. In our app, we receive schema json that describes the layout. For example, we could receive something like this (simplified to make it easier to read)
{
component: 'input'
}
and I have a convertor function that places the component in where one is detected in the structure. It will send something do like: (again, simplified)
import Table from './Table';
covert(schema) {
return {
component: Table // where table is: (props:any) => JSX.Element
}
}
I want to write a test for this, but having trouble with the comparing the result with the expected. In my test, I mock the Table component and send through a named mock function as the second param. Then I use the same named param in the expected results.
I get an error back: The second argument ofjest.mockmust be an inline function. I can change this to an inline function, but then how can I use this in the expected structure used for comparison?
// Test code
import React from 'react';
const mockComponent = () => <div>table</div>
jest.mock('./Table', mockComponent);
const schema = {
component: 'table'
}
describe('Component Structure', () => {
test('componentizes the schema structure', () => {
const results = convert(schema);
const expected = {
component: mockComponent
};
expect(results).toEqual(expected);
});
});
The error is because you are not mocking the component properly, the right way should be:
jest.mock('./Table', () => mockComponent);
given that you already have mockComponent defined as:
const mockComponent = () => <div>table</div>
or you could do:
jest.mock('./Table', () => () => <div />);
The proper mocking of the components would be something like this:
const mockComponent = () => <div>table</div>
jest.mock('./Table', () => mockComponent)
Related
Cypress has a way to expose the app's state to the test runner -- in React it usually looks like this:
class MyComponent extends React.Component {
constructor (props) {
super(props)
// only expose the app during E2E tests
if (window.Cypress) {
window.app = this
}
}
...
}
Then you could access your state in a test with
cy.window()
.its('app.state')
.should('deep.equal', myStateObject)
However, the setup for Remix projects relies on functional components. I've tried this in my root.tsx component with a useEffect call:
export default function App() {
useEffect(() => {
window.app = App;
}, []}
}
as well as in the root route (routes/index.tsx) by importing the <App /> component and using the logic in the useEffect function above. Neither of these options are working and I'm not sure where else to go here. Remix's GitHub issues are devoid of questions about this issue, so maybe I'm going about this the wrong way. Any help is appreciated! Thanks!
I haven't done much work with Remix, but there is a question here that might be useful:
React - getting a component from a DOM element for debugging.
Note the last paragraph
Function components
Function components don't have "instances" in the same way classes do, so you can't just modify the FindReact function to return an object with forceUpdate, setState, etc. on it for function components.
That said, you can at least obtain the React-fiber node for that path, containing its props, state, and such. To do so, modify the last line of the FindReact function to just: return compFiber;
There's a lib cypress-react-app-actions that implements this for Cypress
export const getReactFiber = (el) => {
const key = Object.keys(el).find((key) => {
return (
key.startsWith('__reactFiber$') || // react 17+
key.startsWith('__reactInternalInstance$') // react <17
)
})
if (!key) {
return
}
return el[key]
}
// react 16+
export const getComponent = (fiber) => {
let parentFiber = fiber.return
while (typeof parentFiber.type == 'string') {
parentFiber = parentFiber.return
}
return parentFiber
}
One of the example tests is
/// <reference types="cypress" />
import { getReactFiber, getComponent } from '../support/utils'
it('calls Example double()', () => {
cy.visit('/')
cy.get('.Example').within(() => { // select via className of component
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.root().then((el$) => {
const fiber = getReactFiber(el$[0])
console.log(fiber)
const component = getComponent(fiber)
console.log(component.stateNode)
cy.log('calling **double()**')
component.stateNode.double() // work with component for functional
})
cy.contains('[data-cy=count]', '4')
})
})
This example is for class components, but given the info in Function components section above, you would use the component object rather than component.stateNode.
I have a hoc component like this:
export const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => (
<WrappedComponent {...props} data-test-id="this-is-a-element" />
);
return ModifiedComponent;
};
export default withAttrs;
and I use it like this:
import React from 'react';
import withAttrs from './withAttrs';
const SomeLink = () => <a><p>hey</p</a>;
export default withAttrs(SomeLink);
I expect to have an anchor tag like this:
<a data-test-id="this-is-a-element"><p>hey</p></a>
But the hoc doesn't add the data-attribute to the first element. Is there a way to achieve this?
But the hoc doesn't add the data-attribute to the first element.
It's not the HOC that isn't adding it, it's SomeLink, which doesn't do anything with the props the HOC passes to it.
The simple answer is to update SomeLink:
const SomeLink = (props) => <a {...props}><p>hey</p></a>;
That's by far the better thing to do than the following.
If you can't do that, you could make your HOC add the property after the fact, but it seems inappropriate to have the HOC reach inside the component and change things. In fact, React makes the element objects it creates immutable, which strongly suggests you shouldn't try to mess with them.
Still, it's possible, it's probably just a bad idea:
export const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => {
// Note we're *calling* the function, not just putting it in
// a React element via JSX; we're using it as a subroutine of
// this component rather than as its own component.
// This will only work with function components. (You could
// write a version that handles class components as well,
// but offhand I don't think you can make one HOC that handles
// both in this case.)
const result = WrappedComponent(props);
return {
...result,
props: {
...result.props,
"data-test-id": "this-is-a-element",
},
};
};
return ModifiedComponent;
};
/*export*/ const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => {
// Note we're *calling* the function, not just putting it in
// a React element via JSX; we're using it as a subroutine of
// this component rather than as its own component.
// This will only work with function components. (You could
// write a version that handles class components as well,
// but offhand I don't think you can make one HOC that handles
// both in this case.)
const result = WrappedComponent(props);
// THIS IS PROBABLY A VERY BAD IDEA. React makes these objects
// immutable, probably for a reason. We shouldn't be mucking
// with them.
return {
...result,
props: {
...result.props,
"data-test-id": "this-is-a-element",
},
};
};
return ModifiedComponent;
};
const SomeLink = () => <a><p>hey</p></a>;
const SomeLinkWrapped = withAttrs(SomeLink);
const Example = () => {
return <div>
<div>Unwrapped:</div>
<SomeLink />
<div>Wrapped:</div>
<SomeLinkWrapped />
</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
/* So we can see that it was applied */
[data-test-id=this-is-a-element] {
color: green;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Again, I don't think I'd do that except as a very last resort, and I wouldn't be surprised if it breaks in future versions of React.
I need to have a component for handling settings, this component (called Settings) stores state using useState(), for example the primary color.
I need to create a single instance of this component and make it available to every component in the app. Luckily, I already pass down a state dict to every component (I'm very unsure if this is the correct way to achieve that btw), so I can just include this Settings constant.
My problem is that I don't know how to create the component for this purpose, so that I can call its functions and pass it to children.
Here is roughly what my Settings component looks like:
const Settings = (props) => {
const [primaryColor, setPrimaryColor] = useState("")
const getColorTheme = (): string => {
return primaryColor
}
const setColorTheme = (color: string): void => {
setPrimaryColor(color)
}
return null
}
export default Settings
Then I would like to be able to do something like this somewhere else in the app:
const App = () => {
const settings = <Settings />
return (
<div style={{ color: settings.getColorTheme() }}></div>
)
}
Bear in mind that I'm completely new to react, so my approach is probably completely wrong.
You can use a custom Higher Order Component(HOC) for this purpose, which is easier than creating a context(even thougn context is also a HOC). A HOC takes a component and returns a new component. You can send any data from your HOC to the received component.
const withSettings = (Component) => {
const [settings, setSettings] = useState({})
// ...
// ...
<Component {...props} settings={settings}/>
);
And you can use it like this:
const Component = ({ settings }) => {
...your settings UI
}
export default SettingsUI = withSettings(Component);
You can read more about HOCs in the official react documentation
I have a small test that simulates a click (hoping to do more with the test, but this is where I'm stuck so far):
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import HamburgerIcon from './HamburgerIcon';
Enzyme.configure({ adapter: new Adapter() });
test('hamburger icon changes class and state on click', () => {
const wrapper = mount(<HamburgerIcon />);
const hamburgerIcon = wrapper.find('div#mobile-nav');
hamburgerIcon.simulate('click');
});
When running this test, I get the error:
TypeError: this.props.showOverlay is not a function
After doing some reading, I've realized that this isn't working because the simulated click calls a function that is two levels up from the component that is being tested (HamburgerIcon).
When I first tried to run this, I was using Enzyme's shallow, and I since changed it to mount thinking that this would give the test access to the showOverlay function, but I was wrong.
Then I read that this might be a good use case for a mock function, and I tried to start implementing this:
...
const showOverlay = jest.fn();
// how to set this.props.ShowOverlay to the const above??
test('has div with class .closed', () => {
const wrapper = mount(<HamburgerIcon />);
const hamburgerIcon = wrapper.find('div#mobile-nav');
hamburgerIcon.simulate('click');
});
This is where I am sort of lost -- I'm not sure if mock functions are the right direction here, and I'm also not sure how the syntax of setting up the mock function will work.
Continue with shallow if you're just unit testing a single component. If this component is nested and you're testing against child nodes, then you'll mount the parent.
That said, you're on the right track for using a mock function. Simply pass it into the component like so:
<HamburgerIcon showOverlay={showOverlay} />
For example:
const showOverlay = jest.fn();
test('shows an overlay', () => {
const wrapper = mount(<HamburgerIcon showOverlay={showOverlay} />);
const hamburgerIcon = wrapper.find('div#mobile-nav');
hamburgerIcon.simulate('click');
expect(showOverlay).toHaveBeenCalled();
});
If you have multiple props, then I like to do something more declarative:
// define props here if you want an easy way check if they've been
// called (since we're defining at the top-level, all tests have access
// to this function)
const showOverlay = jest.fn();
// then include them in an "initialProps" object (you can also just define
// them within this object as well, but you'll have to use
// "initialProps.nameOfFunction" to check if they're called -- kind of
// repetitive if you have a lot of props that you're checking against)
const initialProps = {
showOverlay,
someOtherFunction: jest.fn()
}
// use the spread syntax to pass all the props in the "initialProps" object
// to the component
test('shows an overlay', () => {
const wrapper = mount(<HamburgerIcon { ...initialProps } />);
const hamburgerIcon = wrapper.find('div#mobile-nav');
hamburgerIcon.simulate('click');
expect(showOverlay).toHaveBeenCalled(); // here we can just use the mock function name
expect(initialProps.someOtherFunction).toHaveBeenCalledTimes(0); // here we'll have to use "initialProps.property" because it has been scoped to the object
});
I just finished writing my first Reactjs component and I am ready to write some tests (I used material-ui's Table and Toggle).
I read about jest and enzyme but I feel that I am still missing something.
My component looks like this (simplified):
export default class MyComponent extends Component {
constructor() {
super()
this.state = {
data: []
}
// bind methods to this
}
componentDidMount() {
this.initializeData()
}
initializeData() {
// fetch data from server and setStates
}
foo() {
// manuipulatig data
}
render() {
reutrn (
<Toggle
id="my-toggle"
...
onToggle={this.foo}
>
</Toggle>
<MyTable
id="my-table"
data={this.state.data}
...
>
</MyTable>
)
}
}
Now for the test. I want to write a test for the following scenario:
Feed initializeData with mocked data.
Toggle my-toggle
Assert data has changed (Should I assert data itself or it is better practice to assert my-table instead?)
So I started in the very beginning with:
describe('myTestCase', () => {
it('myFirstTest', () => {
const wrapper = shallow(<MyComponent/>);
}
})
I ran it, but it failed: ReferenceError: fetch is not defined
My first question is then, how do I mock initializeData to overcome the need of calling the real code that using fetch?
I followed this answer: https://stackoverflow.com/a/48082419/2022010 and came up with the following:
describe('myTestCase', () => {
it('myFirstTest', () => {
const spy = jest.spyOn(MyComponent.prototype, 'initializeData'
const wrapper = mount(<MyComponent/>);
}
})
But I am still getting the same error (I also tried it with componentDidMount instead of initializeData but it ended up the same).
Update: I was wrong. I do get a fetch is not defined error but this time it is coming from the Table component (which is a wrap for material-ui's Table). Now that I come to think about it I do have a lot of "fetches" along the way... I wonder how to take care of them then.
fetch is supported in the browser, but jest/enzyme run in a Node environment, so fetch isn't a globally available function in your test code. There are a few ways you can get around this:
1: Globally mock fetch - this is probably the simplest solution, but maybe not the cleanest.
global.fetch = jest.fn().mockResolvedValue({
json: () => /*Fake test data*/
// or mock a response with `.text()` etc if that's what
// your initializeData function uses
});
2: Abstract your fetch call into a service layer and inject that as a dependency - This will make your code more flexible (more boilerplate though), since you can hide fetch implementation behind whatever interface you choose. Then at any point in the future, if you decide to use a different fetch library, you can swap out the implementation in your service layer.
// fetchService.js
export const fetchData = (url) => {
// Simplified example, only takes 'url', doesn't
// handle errors or other params.
return fetch(url).then(res => res.json());
}
// MyComponent.js
import {fetchService} from './fetchService.js'
class MyComponent extends React.Component {
static defaultProps = {
// Pass in the imported fetchService by default. This
// way you won't have to pass it in manually in production
// but you have the option to pass it in for your tests
fetchService
}
...
initializeData() {
// Use the fetchService from props
this.props.fetchService.fetchData('some/url').then(data => {
this.setState({ data });
})
}
}
// MyComponent.jest.js
it('myFirstTest', () => {
const fetchData = jest.fn().mockResolvedValue(/*Fake test data*/);
const fetchService = { fetchData };
const wrapper = mount(<MyComponent fetchService={fetchService} />);
return Promise.resolve().then(() = {
// The mock fetch will be resolved by this point, so you can make
// expectations about your component post-initialization here
})
}