In a React vanilla form, I need to solve an issue where users edit manually the value of an option to make the form submit bad values.
To reproduce, the users inspect the element, and manually change the value of an option to an invalid value (domain-wise).
The fix is easy, but I want to create a failing unit test before fixing, in a TDD fashion, and cannot figure how to model this in a test. I use jest and react-testing-library.
Here is the form code:
export const CreateTripForm = ({ countries, onSubmit }) => {
const [countryId, setCountryId] = useState()
const submit = async (e) => {
e.preventDefault()
if (countryId === undefined) return
await onSubmit(countryId)
}
return (
<form onSubmit={submit}>
<legend>Choose a country</legend>
<label htmlFor="countryId">Country</label>
<select name="countryId" required value={countryId} onChange={(e) => setCountryId(e.target.value)}>
<option value=""></option>
{countries.map((country) =>
<option key={country.id} value={country.id}>{country.label}</option>
)}
</select>
<input type="submit" value="Create a trip" />
</form>
)
}
Here is what I tried to do, but the test passes instead of failing:
it('keeps previous countryId if the selected one has been tampered with', () => {
const onSubmit = jest.fn(() => Promise.resolve())
const countries = [
{ id: 'fr', label: 'France' },
{ id: 'en', label: 'England' },
]
const { container } = render(
<CreateTripForm countries={countries} onSubmit={onSubmit} />
)
const select = container.querySelector('select[name=countryId]')
const submitButton = container.querySelector('input[type=select]')
// Select the 'fr' option, it works.
fireEvent.change(select, { target: { value: 'fr' } })
submitButton.click()
expect(onSubmit).toHaveBeenCalledWith('fr')
// Edit an option to have an incorrect value, it should keep the previous value.
elements.unitSelect.options[2].value = 'asgard'
fireEvent.change(select, { target: { value: 'asgard' } })
submitButton.click()
expect(onSubmit).toHaveBeenCalledWith('fr')
})
I've had some bad experiences with expect() like this before.
Have you tried to separate the tests? One for success and one for failure?
From what I can see, the first expect() doesn't reset what he have been called already so I guess that's why your test have passed on the second expect().
Try it like this:
it('keeps previous countryId if the selected one has been tampered with', () => {
const onSubmit = jest.fn(() => Promise.resolve())
const countries = [
{ id: 'fr', label: 'France' },
{ id: 'en', label: 'England' },
]
const { container } = render(
<CreateTripForm countries={countries} onSubmit={onSubmit} />
)
const select = container.querySelector('select[name=countryId]')
const submitButton = container.querySelector('input[type=select]')
// Select the 'fr' option, it works.
fireEvent.change(select, { target: { value: 'fr' } })
submitButton.click()
expect(onSubmit).toHaveBeenCalledWith('fr')
})
it('should prevent sending with tampered select', () => {
const onSubmit = jest.fn(() => Promise.resolve())
const countries = [
{ id: 'fr', label: 'France' },
{ id: 'en', label: 'England' },
]
const { container } = render(
<CreateTripForm countries={countries} onSubmit={onSubmit} />
)
const select = container.querySelector('select[name=countryId]')
const submitButton = container.querySelector('input[type=select]')
elements.unitSelect.options[2].value = 'asgard'
fireEvent.change(select, { target: { value: 'asgard' } })
submitButton.click()
expect(onSubmit).not.toHaveBeenCalled()
})
Related
I have a component like this:
export const MyComponent = props => {
return (
{
props.options.map(option =>
<>
<div>
<input type="radio" id={option.id} name="group" value={option.id} />
<label htmlFor={option.id}>{option.label}</label>
</div>
<span>Some other text</span>
</>
)
}
)
}
And in my test, I want to make sure that both that all the radio buttons are rendered with the right label text and the extra text in the span are present.
import { render, screen, within } from '#testing-library/react'
describe('MyComponent', () => {
const props = {
options: [
{ id: 1, label: 'Apple' },
{ id: 2, label: 'Banana' },
{ id: 3, label: 'Cherry' },
]
}
it('Renders the component', () => {
render(<MyComponent {...props} />)
const options = screen.queryAllByRole('radio')
expect(options).toBeArrayOfSize(3)
options.forEach((option, index) => {
const { getByText } = within(option)
expect(getByText(props.options[index])).toBeInTheDocument() // Assertion fails
expect(getByText("Some other text")).toBeInTheDocument() // Also fails
})
})
})
However, I'm getting errors on the two expect assertions.
You can try the following:
import { render, screen } from "#testing-library/react"
import { MyComponent } from "./MyComponent"
describe("MyComponent", () => {
const props = {
options: [
{ id: 1, label: "Apple" },
{ id: 2, label: "Banana" },
{ id: 3, label: "Cherry" },
],
}
it("Renders the component", () => {
render(<MyComponent {...props} />)
const options = screen.queryAllByRole("radio")
expect(options).toHaveLength(3)
props.options.forEach((option) => {
const label = screen.getByLabelText(option.label)
const radioBtn = screen.getByRole("radio", { name: option.label })
// Need to use getAllByText query since the string "Some other text" is repeated... getByText will throw because of multiple matches
const [someOtherText] = screen.getAllByText("Some other text")
expect(label).toBeInTheDocument()
expect(radioBtn).toBeInTheDocument()
expect(someOtherText).toHaveTextContent(someOtherText.textContent)
})
})
})
I'm attempting to test a Select input inside an Ant Design Form filled with initialValues and the test is failing because the Select does not receive a value. Is there a best way to test a "custom" rendered select?
Test Output:
Error: expect(element).toHaveValue(chocolate)
Expected the element to have value:
chocolate
Received:
Example Test:
import { render, screen } from '#testing-library/react';
import { Form, Select } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => children,
...options,
});
describe('select tests', () => {
it('renders select', () => {
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
customRender(
<Form initialValues={{ formSelectItem: value }}>
<Form.Item label="Form Select Label" name="formSelectItem">
<Select options={options} />
</Form.Item>
</Form>,
);
expect(screen.getByLabelText('Form Select Label')).toHaveValue(value);
});
});
testing a library component may be harsh sometimes because it hides internal complexity.
for testing antd select i suggest to mock it and use normal select in your tests like this:
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const Select = ({ children, onChange, ...rest }) => {
return <select role='combobox' onChange={e => onChange(e.target.value)}>
{children}
</select>;
};
Select.Option = ({ children, ...otherProps }) => {
return <option role='option' {...otherProps}}>{children}</option>;
}
return {
...antd,
Select,
}
})
this way you can test the select component as a normal select (use screen.debug to check that the antd select is mocked)
I mocked a normal select and was able to get everything working.
The following example utilizes Vitest for a test runner but should apply similar to Jest.
antd-mock.tsx
import React from 'react';
import { vi } from 'vitest';
vi.mock('antd', async () => {
const antd = await vi.importActual('antd');
const Select = props => {
const [text, setText] = React.useState('');
const multiple = ['multiple', 'tags'].includes(props.mode);
const handleOnChange = e => props.onChange(
multiple
? Array.from(e.target.selectedOptions)
.map(option => option.value)
: e.target.value,
);
const handleKeyDown = e => {
if (e.key === 'Enter') {
props.onChange([text]);
setText('');
}
};
return (
<>
<select
// add value in custom attribute to handle async selector,
// where no option exists on load (need to type to fetch option)
className={props.className}
data-testid={props['data-testid']}
data-value={props.value || undefined}
defaultValue={props.defaultValue || undefined}
disabled={props.disabled || undefined}
id={props.id || undefined}
multiple={multiple || undefined}
onChange={handleOnChange}
value={props.value || undefined}
>
{props.children}
</select>
{props.mode === 'tags' && (
<input
data-testid={`${props['data-testid']}Input`}
onChange={e => setText(e.target.value)}
onKeyDown={handleKeyDown}
type="text"
value={text}
/>
)}
</>
);
};
Select.Option = ({ children, ...otherProps }) => (
<option {...otherProps}>{children}</option>
);
Select.OptGroup = ({ children, ...otherProps }) => (
<optgroup {...otherProps}>{children}</optgroup>
);
return { ...antd, Select };
});
utils.tsx
import { render } from '#testing-library/react';
import { ConfigProvider } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => <ConfigProvider prefixCls="bingo">{children}</ConfigProvider>,
...options,
});
export * from '#testing-library/react';
export { default as userEvent } from '#testing-library/user-event';
export { customRender as render };
Select.test.tsx
import { Form } from 'antd';
import { render, screen, userEvent } from '../../../test/utils';
import Select from './Select';
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
const initialValues = { selectFormItem: value };
const renderSelect = () => render(
<Form initialValues={initialValues}>
<Form.Item label="Label" name="selectFormItem">
<Select options={options} />
</Form.Item>
</Form>,
);
describe('select tests', () => {
it('renders select', () => {
render(<Select options={options} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('renders select with initial values', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
});
it('handles select change', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
userEvent.selectOptions(screen.getByLabelText('Label'), 'vanilla');
expect(screen.getByLabelText('Label')).toHaveValue('vanilla');
});
});
I want to test a select change function,here is the code :
import React, { useEffect, useState } from 'react';
import Select from 'react-select';
function Component1(props) {
const [content, setContent] = useState('initialized Value');
const [color, setColor] = useState('initialized Value');
const options = [
{ value: 'red', label: 'Red' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
];
useEffect(async () => {
fetchSomeData();
// onclickButton();
}, []);
const fetchSomeData = async () => {
console.log('fetchSomeData');
};
const onclickButton = () => {
console.log('do something here onclickButton');
setContent('updated Value');
};
const resetColor = (value) => {
console.log(value);
setColor(value);
};
return (
<div data-testid='Component1'>
Component1
<button data-testid='button' onClick={onclickButton}>
Button
</button>
<div>Button Test :{content}</div>
<Select aria-label='select-Label' data-testid='select' options={options} value={color} onChange={resetColor} />
<div data-testid='color-value'>Current Color:{color}</div>
</div>
);
}
I did some reasearches , and they said the best way is mocked a select and test it:
beforeEach(() => {
render(<Component1 />);
});
test('should 3', () => {
jest.doMock('react-select', () => ({ options, value, onChange }) => {
function handleChange(event) {
const option = options.find((option) => option.value === event.currentTarget.value);
onChange(option);
}
return (
<select data-testid='custom-select' value={value} onChange={handleChange}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
);
});
fireEvent.change(screen.getByTestId('select'), {
target: { value: 'green' },
});
test('should 2', () => {
// screen.debug()
const onclickButton = jest.fn();
// render(<Component1 onclickButton={onclickButton} />);
fireEvent.click(screen.getByTestId('button'), {
// target: { value: 'JavaScript' },
});
});
after I run the test, I got this :
TestingLibraryElementError: Unable to find an element by: [data-testid="select"]
can some one help me? I just want below codes can be covered by unit test
update:
I tried to use queryByLabelText, and it works, but still ,it seems not trigger the onChange event. I got this:
Expected element to have text content:
Current Color:green
Received:
Current Color:red
fireEvent.select(screen.queryByLabelText('select-Label'),{target:{value:'green'}})
expect(screen.queryByTestId('color-value')).toHaveTextContent('Current Color:green');
I resolved it by below code:
const DOWN_ARROW = { keyCode: 40 };
fireEvent.keyDown(screen.getByLabelText('select-Label'), DOWN_ARROW);
fireEvent.click(screen.getByText('Green'));
these code will trigger onChange event.
also refter to:
how to test react-select with react-testing-library
How to make field validation?
I have an object with fields from which I generate a form, and when submitting, I need to check each field so that it is not empty, I do this, but it doesn’t work
My form:
const [volunteerData, setVolunteerData] = useState({
fullName: {
value: '',
type: "text",
placeholder: "Name",
label: "Name"
},
phone: {
value: '',
type: "text",
placeholder: "Phone number",
label: "Phone number",
mask: "+7(999) 999 99 99"
}
)}
Render form:
const onHandleRenderForm = () => {
return Object.keys(volunteerData).map((items, idx) => {
const control = volunteerData[items];
return (
<div key={idx} className="content-data-box">
<label>{control.label}</label>
<InputMask
type={control.type}
placeholder={control.placeholder}
mask={control.mask}
onChange={e => onHandleFormData(e, items)}
/>
</div>
)
})
};
onChange input:
const onHandleFormData = (e, items) => {
const before = {...volunteerData};
const after = {...before[items]}
after.value = e.target.value;
before[items] = after;
setVolunteerData(before);
};
onClick (submit button):
const onHandleErrorBoundary = () => {
Object.keys(volunteerData).map(items => {
const errorData = items.value === '';
console.log(errorData)
})
};
Change items.value === '' to volunteerData[items].value !== ""
const onHandleErrorBoundary = () => {
Object.keys(volunteerData).map(items => {
const errorData = volunteerData[items].value !== "";
return console.log(errorData);
});
};
you can check here codesandbox
This is register form, I want to send the value orgTypes>name to orgType of data of same object using onChange of select.
https://codesandbox.io/s/react-typescript-zm1ov?fontsize=14&fbclid=IwAR06ZifrKrDT_JFb0A-d_iu5YaSyuQ9qvLRgqS20JgAcSwLtAyaOFOoj5IQ
When I use onChange of Select, it erases all other data of the inputs.
import React from "react";
import { Select } from "antd";
const { Option } = Select;
const RegisterFinal = () => {
const data = {
orgName: "",
orgRegNo: "",
orgType: "",
orgTypes: [
{ id: "1", name: "Vendor" },
{ id: "2", name: "Supplier" },
{ id: "3", name: "Vendor and Supplier" }
],
errors: {}
};
const [values, setValues] = React.useState(data);
const handleChange = (e: any) => {
e.persist();
setValues((values: any) => ({
...values,
[e.target.name]: e.target.value
}));
};
const handleSubmit = () => {
console.log(values);
};
return (
<React.Fragment>
<input
name="orgName"
onChange={handleChange}
placeholder="Organization Name"
value={values.orgName}
/>
<input
name="orgRegNo"
onChange={handleChange}
placeholder="Registration Number"
value={values.orgRegNo}
/>
<Select //imported from antd
id="select"
defaultValue="Choose"
onChange={(select: any) => {
setValues(select);
console.log(select); //shows Vender/Supplier, I want this value to be sent to orgType above
}}
>
{data.orgTypes.map((option: any) => (
<Option key={option.id} value={option.name}>
{option.name}
</Option>
))}
</Select>
</React.Fragment>
);
};
When I use onChange of Select, it erases all other data of the inputs.
Thank you for your help
setValues function expects to take the values object as an argument. Instead you are passing the select value to it.
Instead, you can do it like this:
const handleSelectChange = (selectValue: any) => {
setValues((values: any) => ({
...values,
orgType: selectValue
}));
}
and use handleSelectChange in the onChange of your select.
Check the solution here: https://codesandbox.io/s/react-typescript-p9bjz