-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add updateProps function #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
What possible downsides would there be for an |
The downside is that it contradicts the guiding principles:
A user never updates your components props. They'll click a button which in will update some state which will cause a component to re-render, which will update props. So what you should normally do is click that button instead. But this is tricky for highly reusable components like downshift where you don't know what could update the props of the component. It could be anything. This is why I'm thinking about including it... At the same time, the workaround is so simple I'm not sure it's worth it... |
When you want to update props, I believe the user should typically call it out like as you have mentioned:
this is natural to react developers and I like it remains the same. Most of the use case, the props gets updated automatically (parents passing state as props to children etc), well in those cases I guess we need to have |
I thought I understood what you were saying until you said this:
I thought you were saying that you prefer the |
@kentcdodds : Yup you are right, it was typo :) It should be:
|
Cool 👍 I think I'm going to drop this for now. We'll let someone else resurface it if they want to try to convince us to reverse this decision. |
Hey there! I'm here to bring this back up as the very first test suite that I converted from enzyme to RTL hit this. When developing components for a UI library, I want to have confidence in some of the internals of the componentry. Specifically in that when a consumer provides specific props, the effects of those props can be verified and regulated. Take the following example of a Button component. // src/components/Button/Button.js
import React, { Component } from 'react';
import styled from 'styled-components';
import classnames from 'classnames';
class Button extends Component<Props> {
static defaultProps = {
theme: 'secondary',
isDisabled: false,
onClick: () => {},
};
render() {
const { label, isDisabled, theme, onClick } = this.props;
return (
<StyledButton
disabled={isDisabled}
aria-disabled={isDisabled}
role="button"
className={classnames({
't-primary': theme === 'primary',
't-secondary': theme === 'secondary' || !theme,
})}
onClick={onClick}
>
{label}
</StyledButton>
);
}
}
const StyledButton = styled.button`
box-sizing: border-box;
min-height: 30px;
max-height: 30px;
min-width: 120px;
max-width: 120px;
border-radius: 4px;
line-height: 20px;
font-size: 14px;
font-weight: 500;
vertical-align: middle;
cursor: pointer;
user-select: none;
&.t-primary {
background-color: steelblue;
color: #ffffff;
}
&.t-secondary {
background-color: #ffffff;
color: steelblue;
}
&[aria-disabled='true'] {
background-color: #dfdfdf;
color: #a8a8a8;
cursor: not-allowed;
}
`;
export default Button; // src/components/Button/__tests__/Button.spec.js
import React from 'react';
import { render, Simulate, flushPromises } from 'react-testing-library';
import Button from '../Button';
const updateProps = (component, container) => render(component, { container });
describe('<Button />', () => {
const fixtureRequiredProps = { label: 'Search' };
const defaultProps = { ...Button.defaultProps, ...fixtureRequiredProps };
const { getByTestId, container } = render(<Button {...defaultProps} />);
describe('render', () => {
beforeEach(() => {
updateProps(<Button {...defaultProps} />, container);
});
it('executes without issue when only required props are provided', () => {
expect(container.firstChild).not.toBeNull();
});
it('renders `label` as text', () => {
expect(container.firstChild.textContent).toEqual(
fixtureRequiredProps.label,
);
});
it('renders with class `t-secondary` by default', () => {
expect(container.firstChild.classList.contains('t-secondary')).toEqual(
true,
);
});
it('renders with class `t-primary` when `theme="primary"`', () => {
updateProps(
<Button {...defaultProps} theme="primary" />,
container,
);
expect(container.firstChild.classList.contains('t-primary')).toEqual(
true,
);
});
it('renders with class `t-secondary` when `theme="secondary"`', () => {
updateProps(
<Button {...defaultProps} theme="secondary" />,
container,
);
expect(container.firstChild.classList.contains('t-secondary')).toEqual(
true,
);
});
it('renders with [disabled] when `isDisabled`', () => {
expect(container.firstChild.disabled).toEqual(false);
updateProps(<Button {...defaultProps} isDisabled />, container);
expect(container.firstChild.disabled).toEqual(true);
});
});
}); One of the first things I did was create an updateProps helper. Funny enough once I finished updating these tests, I went to look for a related issue and found that the exact same name was used! haha. This can also be helpful when testing any controlled component and verification of a11y attributes. I am a strong advocate for having it in the library. However, we do need to be clear in the purpose of why it exists, or people will just abuse it for things it is not intended for. Unless I'm way off base in my thinking, having been 'brainwashed' by current testing 'best practices', this example should prove the value a |
Thanks for the added perspective @SavePointSam :) In your case I would actually say that pattern is not a good one. Why do you need it to render to the same container? Your component doesn't necessarily respond differently when its props update. It just happens to render differently based on the current state of props. You could just as easily rewrite that test by rendering the entire component in every individual test: // src/components/Button/__tests__/Button.spec.js
import React from 'react';
import { render, Simulate, flushPromises } from 'react-testing-library';
import Button from '../Button';
const renderButton = props => render(<Button {...defaultProps} {...props} />)
describe('<Button />', () => {
const defaultProps = { label: 'Search' };
// you don't need to include Button.defaultProps in this because that's actually
// how react works. If those props aren't provided the defaults would be used.
describe('render', () => {
it('executes without issue when only required props are provided', () => {
expect(renderButton().container.firstChild).not.toBeNull();
});
it('renders `label` as text', () => {
expect(renderButton().container.firstChild.textContent).toEqual(
fixtureRequiredProps.label,
);
});
it('renders with class `t-secondary` by default', () => {
expect(renderButton().container.firstChild.classList.contains('t-secondary')).toEqual(
true,
);
});
it('renders with class `t-primary` when `theme="primary"`', () => {
const {container} = renderButton({theme: 'primary'})
expect(container.firstChild.classList.contains('t-primary')).toEqual(
true,
);
});
it('renders with class `t-secondary` when `theme="secondary"`', () => {
const {container} = renderButton({theme: 'secondary'})
expect(container.firstChild.classList.contains('t-secondary')).toEqual(
true,
);
});
it('renders with [disabled] when `isDisabled`', () => {
expect(renderButton().container.firstChild.disabled).toEqual(false);
expect(renderButton({isDisabled: true}).container.firstChild.disabled).toEqual(true);
});
});
}); In addition, by sharing the My example above is just one way to do things, there are others. But I would suggest that the way your test was before was unnecessarily brittle. The only time you might need an If One last thing, please understand that I'm not criticising you or your coding abilities. You're a great person :) Our testing tools up to this point have encouraged sub-optimal testing patterns. No worries :) |
Hey! First, I take no offense to your suggestions and actually welcome them! I totally didn't even realize the state leak 😱. Like I said:
And you proved that I was, in fact, incorrect. I'm really happy I hit this problem and thank you for explaining how I can better test my components! That's exactly why I've begun moving to RTL. To have better tests without the misconceptions that exists out there today. Your explanation about when Thanks! |
Would you like to do that @SavePointSam? :) |
Aww. My first contribution. haha |
* fix: use exact match for data-testid [fixes testing-library#8] * add tests for exact matches * disallow substring match * fix: heading level for getByTestId * Add docs for ExactTextMatch * Update README.md
Sorry for bringing this up again @kentcdodds, but I'm not sure if I'd really have a need for this function, or if my current approach isn't 'best-practice'. Case is, I have a The tests I first wrote, all passed. describe('DateInput', () => {
it('renders the correct date', () => {
const date = new Date('2017-06-13T04:41:20');
const { getByTestId } = render(
<DateInput value={date} formatFunction={formatDate} />
);
const input = getByTestId('date-input');
expect(input.value).toBe('13/06/2017');
});
it('changes the date on click', () => {
let date = new Date('2017-06-13T04:41:20');
const onChange = jest.fn(selectedDate => {
date = selectedDate;
});
const { getByTestId, getByText, rerender } = render(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
const input = getByTestId('date-input');
expect(input.value).toBe('13/06/2017');
fireEvent.click(getByText('19'));
rerender(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
expect(input.value).toBe('19/06/2017');
});
it('switches to previous month when clicking 31', () => {
let date = new Date('2017-06-13T04:41:20');
const onChange = jest.fn(selectedDate => {
date = selectedDate;
});
const { getByText, rerender } = render(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
fireEvent.click(getByText('31'));
rerender(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
expect(getByText('May 2017'));
});
}); As you can see, there's a lot of repetition in each test, and since I want my tests to be maintainable, I searched for some examples, and I stumbled upon this. I tried to refactor my code, by abstracting the initial component render, as such: const renderDateInput = () => {
let date = new Date('2017-06-13T04:41:20');
const onChange = jest.fn(selectedDate => {
date = selectedDate;
});
return render(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
};
describe('DateInput', () => {
it('renders the correct date', () => {
const { getByTestId } = renderDateInput();
const input = getByTestId('date-input');
expect(input.value).toBe('13/06/2017');
});
it('changes the date on click', () => {
const { getByTestId, getByText, rerender } = renderDateInput();
const input = getByTestId('date-input');
expect(input.value).toBe('13/06/2017');
fireEvent.click(getByText('19'));
rerender(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
expect(input.value).toBe('19/06/2017');
});
it('switches to previous month when clicking 31', () => {
let date = new Date('2017-06-13T04:41:20');
const onChange = jest.fn(selectedDate => {
date = selectedDate;
});
const { getByText, rerender } = render(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
fireEvent.click(getByText('31'));
rerender(
<DateInput value={date} onChange={onChange} formatFunction={formatDate} />
);
expect(getByText('May 2017'));
});
}); The first test still passes, but as soon as I started simplifying the second test, I was blocked. So now I'm wondering how to best approach my test, while having next to no repetition. Thanks a bunch! |
Hi @JDansercoer, Do this: function TestDateWrapper() {
const [date, setDate] = React.useState(new Date('2017-06-13T04:41:20'))
return <DateInput value={date} onChange={setDate} formatFunction={formatDate} />
} Then render |
Aha of course, that makes perfect sense! Thanks for the quick answer! |
After playing around with it some more, I noticed that my |
test: 💍 fix test for toggle focus
As noted in the docs, if I want to update props I simply call render with the same container:
But this is cumbersome and it'd be nice to just have an
updateProps
function instead. It could have a note to avoid using it when you can, but it would be nice to have.I'm not 100% certain that I really want it. Just thinking... One of the nice things about this library is what it doesn't give you and how that encourages better tests. But maybe this wont be so bad... It's especially useful for reusable components (like downshift).
The text was updated successfully, but these errors were encountered: