React 测试库对 Material UI Select 组件的更改
React testing library on change for Material UI Select component
我正在尝试测试 Select component using react-testing-library 的 onChange
事件。
我使用 getByTestId
抓取元素,效果很好,然后设置元素的值,然后调用 fireEvent.change(select);
,但从未调用 onChange
并且从未更新状态.
我尝试过使用 select 组件本身以及获取对底层 input
元素的引用,但都不起作用。
有什么解决办法吗?或者这是一个已知问题?
当您将 Material-UI 的 Select
与 native={false}
(默认设置)一起使用时,这会变得非常复杂。这是因为呈现的输入甚至没有 <select>
HTML 元素,而是混合了 div、隐藏输入和一些 svg。然后,当您单击 select 时,将显示一个表示层(有点像模态),其中包含您的所有选项(顺便说一下,它们不是 <option>
HTML 元素) ,我相信是这些选项之一的点击触发了您作为 onChange
回调传递给原始 Material-UI <Select>
的任何内容
综上所述,如果您愿意使用 <Select native={true}>
,那么您将拥有实际的 <select>
和 <option>
HTML 元素,并且您可以像预期的那样在 <select>
上触发更改事件。
这是来自代码沙箱的测试代码,它可以运行:
import React from "react";
import { render, cleanup, fireEvent } from "react-testing-library";
import Select from "@material-ui/core/Select";
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
cleanup();
});
it("calls onChange if change event fired", () => {
const mockCallback = jest.fn();
const { getByTestId } = render(
<div>
<Select
native={true}
onChange={mockCallback}
data-testid="my-wrapper"
defaultValue="1"
>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</Select>
</div>
);
const wrapperNode = getByTestId("my-wrapper")
console.log(wrapperNode)
// Dig deep to find the actual <select>
const selectNode = wrapperNode.childNodes[0].childNodes[0];
fireEvent.change(selectNode, { target: { value: "3" } });
expect(mockCallback.mock.calls).toHaveLength(1);
});
您会注意到,一旦 Material-UI 渲染出其 <Select>
,您必须向下挖掘节点才能找到实际 <select>
的位置。但是一旦找到它,就可以对其进行 fireEvent.change
。
可在此处找到 CodeSandbox:
import * as React from "react";
import ReactDOM from 'react-dom';
import * as TestUtils from 'react-dom/test-utils';
import { } from "mocha";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
describe("Testing Select component", () => {
test('start empty, open and select second option', (done) => {
//render the component
ReactDOM.render(<Select
displayEmpty={true}
value={""}
onChange={(e) => {
console.log(e.target.value);
}}
disableUnderline
classes={{
root: `my-select-component`
}}
>
<MenuItem value={""}>All</MenuItem>
<MenuItem value={"1"}>1</MenuItem>
<MenuItem value={"2"}>2</MenuItem>
<MenuItem value={"3"}>3</MenuItem>
</Select>, container);
//open filter
TestUtils.Simulate.click(container.querySelector('.my-select-component'));
const secondOption = container.ownerDocument.activeElement.parentElement.querySelectorAll('li')[1];
TestUtils.Simulate.click(secondOption);
done();
});
});
it('Set min zoom', async () => {
const minZoomSelect = await waitForElement( () => component.getByTestId('min-zoom') );
fireEvent.click(minZoomSelect.childNodes[0]);
const select14 = await waitForElement( () => component.getByText('14') );
expect(select14).toBeInTheDocument();
fireEvent.click(select14);
});
material-ui 的 select 组件使用 mouseDown 事件触发弹出菜单出现。如果您使用 fireEvent.mouseDown
应该会触发弹出窗口,然后您可以在出现的列表框中单击 selection。请参阅下面的示例。
import React from "react";
import { render, fireEvent, within } from "react-testing-library";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Typography from "@material-ui/core/Typography";
it('selects the correct option', () => {
const {getByRole} = render(
<>
<Select fullWidth value={selectedTab} onChange={onTabChange}>
<MenuItem value="privacy">Privacy</MenuItem>
<MenuItem value="my-account">My Account</MenuItem>
</Select>
<Typography variant="h1">{/* value set in state */}</Typography>
</>
);
fireEvent.mouseDown(getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByText(/my account/i));
expect(getByRole('heading')).toHaveTextContent(/my account/i);
});
这是带有 Select 选项的 MUI TextField 的工作示例。
文本域:
import { TextField, MenuItem, InputAdornment } from "@material-ui/core";
import { useState } from "react";
export const sampleData = [
{
name: "Vat-19",
value: 1900
},
{
name: "Vat-0",
value: 0
},
{
name: "Vat-7",
value: 700
}
];
export default function TextSelect() {
const [selected, setSelected] = useState(sampleData[0].name);
return (
<TextField
id="vatSelectTextField"
select
label="#ExampleLabel"
value={selected}
onChange={(evt) => {
setSelected(evt.target.value);
}}
variant="outlined"
color="secondary"
inputProps={{
id: "vatSelectInput"
}}
InputProps={{
startAdornment: <InputAdornment position="start">%</InputAdornment>
}}
fullWidth
>
{sampleData.map((vatOption) => (
<MenuItem key={vatOption.name} value={vatOption.name}>
{vatOption.name} - {vatOption.value / 100} %
</MenuItem>
))}
</TextField>
);
}
测试:
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { act } from "react-dom/test-utils";
import TextSelect, { sampleData } from "../MuiTextSelect/TextSelect";
import "@testing-library/jest-dom";
describe("Tests TextField Select change", () => {
test("Changes the selected value", () => {
const { getAllByRole, getByRole, container } = render(<TextSelect />);
//CHECK DIV CONTAINER
let vatSelectTextField = container.querySelector(
"#vatSelectTextField"
) as HTMLDivElement;
expect(vatSelectTextField).toBeInTheDocument();
//CHECK DIV CONTAINER
let vatSelectInput = container.querySelector(
"#vatSelectInput"
) as HTMLInputElement;
expect(vatSelectInput).toBeInTheDocument();
expect(vatSelectInput.value).toEqual(sampleData[0].name);
// OPEN
fireEvent.mouseDown(vatSelectTextField);
//CHECKO OPTIONS
expect(getByRole("listbox")).not.toEqual(null);
// screen.debug(getByRole("listbox"));
//CHANGE
act(() => {
const options = getAllByRole("option");
// screen.debug(getAllByRole("option"));
fireEvent.mouseDown(options[1]);
options[1].click();
});
//CHECK CHANGED
vatSelectInput = container.querySelector(
"#vatSelectInput"
) as HTMLInputElement;
expect(vatSelectInput.value).toEqual(sampleData[1].name);
});
});
/**
* HAVE A LOOK AT
*
*
* https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Select/Select.test.js
* (ll. 117-121)
*
* https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/TextField/TextField.test.js
*
*
*/
我在使用 Material UI select 元素时遇到了一些问题,但最后我找到了这个简单的解决方案。
const handleSubmit = jest.fn()
const renderComponent = (args?: any) => {
const defaultProps = {
submitError: '',
allCurrencies: [{ name: 'CAD' }, { name: 'EUR' }],
setSubmitError: () => jest.fn(),
handleSubmit,
handleClose,
}
const props = { ...defaultProps, ...args }
return render(<NewAccontForm {...props} />)
}
afterEach(cleanup)
// TEST
describe('New Account Form tests', () => {
it('submits form with corret data', async () => {
const expectedSubmitData = {
account_type: 'Personal',
currency_type: 'EUR',
name: 'MyAccount',
}
const { getByRole, getAllByDisplayValue } = renderComponent()
const inputs = getAllByDisplayValue('')
fireEvent.change(inputs[0], { target: { value: 'Personal' } })
fireEvent.change(inputs[1], { target: { value: 'EUR' } })
fireEvent.change(inputs[2], { target: { value: 'MyAccount' } })
userEvent.click(getByRole('button', { name: 'Confirm' }))
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith(expectedSubmitData)
expect(handleSubmit).toHaveBeenCalledTimes(1)
})
})
})
使用*ByLabelText()
组件
// demo.js
import * as React from "react";
import Box from "@mui/material/Box";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import Typography from "@mui/material/Typography";
export default function BasicSelect() {
const [theThing, setTheThing] = React.useState("None");
const handleChange = (event) => {
setTheThing(event.target.value);
};
return (
<Box sx={{ minWidth: 120 }}>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Choose a thing</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={theThing}
label="Choose a thing"
onChange={handleChange}
>
<MenuItem value={"None"}>None</MenuItem>
<MenuItem value={"Meerkat"}>Meerkat</MenuItem>
<MenuItem value={"Marshmallow"}>Marshmallow</MenuItem>
</Select>
</FormControl>
<Box sx={{ padding: 2 }}>
<Typography>The thing is: {theThing}</Typography>
</Box>
</Box>
);
}
测试
// demo.test.js
import "@testing-library/jest-dom";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Demo from "./demo";
test("When I choose a thing, then the thing changes", async () => {
render(<Demo />);
// Confirm default state.
expect(await screen.findByText(/the thing is: none/i)).toBeInTheDocument();
// Click on the MUI "select" (as found by the label).
const selectLabel = /choose a thing/i;
const selectEl = await screen.findByLabelText(selectLabel);
expect(selectEl).toBeInTheDocument();
userEvent.click(selectEl);
// Locate the corresponding popup (`listbox`) of options.
const optionsPopupEl = await screen.findByRole("listbox", {
name: selectLabel
});
// Click an option in the popup.
userEvent.click(within(optionsPopupEl).getByText(/marshmallow/i));
// Confirm the outcome.
expect(
await screen.findByText(/the thing is: marshmallow/i)
).toBeInTheDocument();
});
codesandbox 注意: 测试不会 运行 在 codesandbox 上,但会 运行 并在本地通过。
我已经在一个页面中完成了多个 Select,试试这个:
import { render, fireEvent, within } from '@testing-library/react'
it('Should trigger select-xxx methiod', () => {
const { getByTestId, getByRole: getByRoleParent } = component
const element = getByTestId('select-xxx');
const { getByRole } = within(element)
const select = getByRole('button')
fireEvent.mouseDown(select);
const list = within(getByRoleParent('listbox')) // get list opened by trigger fireEvent
fireEvent.click(list.getByText(/just try/i)); //select by text
})
这就是我在使用 MUI 5 时的工作方式。
userEvent.click(screen.getByLabelText(/^foo/i));
userEvent.click(screen.getByRole('option', {name: /^bar/i}));
对于有多个选择的人,请确保添加 name
属性
<SelectDropdown
name="date_range"
...
>
...
</SelectDropdown>
<SelectDropdown
name="company"
...
>
...
</SelectDropdown>
// date filter
const date_range_dropdown = getByLabelText('Date Range');
fireEvent.mouseDown(date_range_dropdown);
await screen.findByRole('listbox');
fireEvent.click(
within(screen.getByRole('listbox')).getByText(/Last 30 Days/)
);
// // company filter
const company_dropdown = getByLabelText('Company');
fireEvent.mouseDown(company_dropdown);
fireEvent.click(within(getByRole('listbox')).getByText(/Uber/));
我正在尝试测试 Select component using react-testing-library 的 onChange
事件。
我使用 getByTestId
抓取元素,效果很好,然后设置元素的值,然后调用 fireEvent.change(select);
,但从未调用 onChange
并且从未更新状态.
我尝试过使用 select 组件本身以及获取对底层 input
元素的引用,但都不起作用。
有什么解决办法吗?或者这是一个已知问题?
当您将 Material-UI 的 Select
与 native={false}
(默认设置)一起使用时,这会变得非常复杂。这是因为呈现的输入甚至没有 <select>
HTML 元素,而是混合了 div、隐藏输入和一些 svg。然后,当您单击 select 时,将显示一个表示层(有点像模态),其中包含您的所有选项(顺便说一下,它们不是 <option>
HTML 元素) ,我相信是这些选项之一的点击触发了您作为 onChange
回调传递给原始 Material-UI <Select>
综上所述,如果您愿意使用 <Select native={true}>
,那么您将拥有实际的 <select>
和 <option>
HTML 元素,并且您可以像预期的那样在 <select>
上触发更改事件。
这是来自代码沙箱的测试代码,它可以运行:
import React from "react";
import { render, cleanup, fireEvent } from "react-testing-library";
import Select from "@material-ui/core/Select";
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
cleanup();
});
it("calls onChange if change event fired", () => {
const mockCallback = jest.fn();
const { getByTestId } = render(
<div>
<Select
native={true}
onChange={mockCallback}
data-testid="my-wrapper"
defaultValue="1"
>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</Select>
</div>
);
const wrapperNode = getByTestId("my-wrapper")
console.log(wrapperNode)
// Dig deep to find the actual <select>
const selectNode = wrapperNode.childNodes[0].childNodes[0];
fireEvent.change(selectNode, { target: { value: "3" } });
expect(mockCallback.mock.calls).toHaveLength(1);
});
您会注意到,一旦 Material-UI 渲染出其 <Select>
,您必须向下挖掘节点才能找到实际 <select>
的位置。但是一旦找到它,就可以对其进行 fireEvent.change
。
可在此处找到 CodeSandbox:
import * as React from "react";
import ReactDOM from 'react-dom';
import * as TestUtils from 'react-dom/test-utils';
import { } from "mocha";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
describe("Testing Select component", () => {
test('start empty, open and select second option', (done) => {
//render the component
ReactDOM.render(<Select
displayEmpty={true}
value={""}
onChange={(e) => {
console.log(e.target.value);
}}
disableUnderline
classes={{
root: `my-select-component`
}}
>
<MenuItem value={""}>All</MenuItem>
<MenuItem value={"1"}>1</MenuItem>
<MenuItem value={"2"}>2</MenuItem>
<MenuItem value={"3"}>3</MenuItem>
</Select>, container);
//open filter
TestUtils.Simulate.click(container.querySelector('.my-select-component'));
const secondOption = container.ownerDocument.activeElement.parentElement.querySelectorAll('li')[1];
TestUtils.Simulate.click(secondOption);
done();
});
});
it('Set min zoom', async () => {
const minZoomSelect = await waitForElement( () => component.getByTestId('min-zoom') );
fireEvent.click(minZoomSelect.childNodes[0]);
const select14 = await waitForElement( () => component.getByText('14') );
expect(select14).toBeInTheDocument();
fireEvent.click(select14);
});
material-ui 的 select 组件使用 mouseDown 事件触发弹出菜单出现。如果您使用 fireEvent.mouseDown
应该会触发弹出窗口,然后您可以在出现的列表框中单击 selection。请参阅下面的示例。
import React from "react";
import { render, fireEvent, within } from "react-testing-library";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Typography from "@material-ui/core/Typography";
it('selects the correct option', () => {
const {getByRole} = render(
<>
<Select fullWidth value={selectedTab} onChange={onTabChange}>
<MenuItem value="privacy">Privacy</MenuItem>
<MenuItem value="my-account">My Account</MenuItem>
</Select>
<Typography variant="h1">{/* value set in state */}</Typography>
</>
);
fireEvent.mouseDown(getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByText(/my account/i));
expect(getByRole('heading')).toHaveTextContent(/my account/i);
});
这是带有 Select 选项的 MUI TextField 的工作示例。
文本域:
import { TextField, MenuItem, InputAdornment } from "@material-ui/core";
import { useState } from "react";
export const sampleData = [
{
name: "Vat-19",
value: 1900
},
{
name: "Vat-0",
value: 0
},
{
name: "Vat-7",
value: 700
}
];
export default function TextSelect() {
const [selected, setSelected] = useState(sampleData[0].name);
return (
<TextField
id="vatSelectTextField"
select
label="#ExampleLabel"
value={selected}
onChange={(evt) => {
setSelected(evt.target.value);
}}
variant="outlined"
color="secondary"
inputProps={{
id: "vatSelectInput"
}}
InputProps={{
startAdornment: <InputAdornment position="start">%</InputAdornment>
}}
fullWidth
>
{sampleData.map((vatOption) => (
<MenuItem key={vatOption.name} value={vatOption.name}>
{vatOption.name} - {vatOption.value / 100} %
</MenuItem>
))}
</TextField>
);
}
测试:
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { act } from "react-dom/test-utils";
import TextSelect, { sampleData } from "../MuiTextSelect/TextSelect";
import "@testing-library/jest-dom";
describe("Tests TextField Select change", () => {
test("Changes the selected value", () => {
const { getAllByRole, getByRole, container } = render(<TextSelect />);
//CHECK DIV CONTAINER
let vatSelectTextField = container.querySelector(
"#vatSelectTextField"
) as HTMLDivElement;
expect(vatSelectTextField).toBeInTheDocument();
//CHECK DIV CONTAINER
let vatSelectInput = container.querySelector(
"#vatSelectInput"
) as HTMLInputElement;
expect(vatSelectInput).toBeInTheDocument();
expect(vatSelectInput.value).toEqual(sampleData[0].name);
// OPEN
fireEvent.mouseDown(vatSelectTextField);
//CHECKO OPTIONS
expect(getByRole("listbox")).not.toEqual(null);
// screen.debug(getByRole("listbox"));
//CHANGE
act(() => {
const options = getAllByRole("option");
// screen.debug(getAllByRole("option"));
fireEvent.mouseDown(options[1]);
options[1].click();
});
//CHECK CHANGED
vatSelectInput = container.querySelector(
"#vatSelectInput"
) as HTMLInputElement;
expect(vatSelectInput.value).toEqual(sampleData[1].name);
});
});
/**
* HAVE A LOOK AT
*
*
* https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Select/Select.test.js
* (ll. 117-121)
*
* https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/TextField/TextField.test.js
*
*
*/
我在使用 Material UI select 元素时遇到了一些问题,但最后我找到了这个简单的解决方案。
const handleSubmit = jest.fn()
const renderComponent = (args?: any) => {
const defaultProps = {
submitError: '',
allCurrencies: [{ name: 'CAD' }, { name: 'EUR' }],
setSubmitError: () => jest.fn(),
handleSubmit,
handleClose,
}
const props = { ...defaultProps, ...args }
return render(<NewAccontForm {...props} />)
}
afterEach(cleanup)
// TEST
describe('New Account Form tests', () => {
it('submits form with corret data', async () => {
const expectedSubmitData = {
account_type: 'Personal',
currency_type: 'EUR',
name: 'MyAccount',
}
const { getByRole, getAllByDisplayValue } = renderComponent()
const inputs = getAllByDisplayValue('')
fireEvent.change(inputs[0], { target: { value: 'Personal' } })
fireEvent.change(inputs[1], { target: { value: 'EUR' } })
fireEvent.change(inputs[2], { target: { value: 'MyAccount' } })
userEvent.click(getByRole('button', { name: 'Confirm' }))
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith(expectedSubmitData)
expect(handleSubmit).toHaveBeenCalledTimes(1)
})
})
})
使用*ByLabelText()
组件
// demo.js
import * as React from "react";
import Box from "@mui/material/Box";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import Typography from "@mui/material/Typography";
export default function BasicSelect() {
const [theThing, setTheThing] = React.useState("None");
const handleChange = (event) => {
setTheThing(event.target.value);
};
return (
<Box sx={{ minWidth: 120 }}>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Choose a thing</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={theThing}
label="Choose a thing"
onChange={handleChange}
>
<MenuItem value={"None"}>None</MenuItem>
<MenuItem value={"Meerkat"}>Meerkat</MenuItem>
<MenuItem value={"Marshmallow"}>Marshmallow</MenuItem>
</Select>
</FormControl>
<Box sx={{ padding: 2 }}>
<Typography>The thing is: {theThing}</Typography>
</Box>
</Box>
);
}
测试
// demo.test.js
import "@testing-library/jest-dom";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Demo from "./demo";
test("When I choose a thing, then the thing changes", async () => {
render(<Demo />);
// Confirm default state.
expect(await screen.findByText(/the thing is: none/i)).toBeInTheDocument();
// Click on the MUI "select" (as found by the label).
const selectLabel = /choose a thing/i;
const selectEl = await screen.findByLabelText(selectLabel);
expect(selectEl).toBeInTheDocument();
userEvent.click(selectEl);
// Locate the corresponding popup (`listbox`) of options.
const optionsPopupEl = await screen.findByRole("listbox", {
name: selectLabel
});
// Click an option in the popup.
userEvent.click(within(optionsPopupEl).getByText(/marshmallow/i));
// Confirm the outcome.
expect(
await screen.findByText(/the thing is: marshmallow/i)
).toBeInTheDocument();
});
codesandbox 注意: 测试不会 运行 在 codesandbox 上,但会 运行 并在本地通过。
我已经在一个页面中完成了多个 Select,试试这个:
import { render, fireEvent, within } from '@testing-library/react'
it('Should trigger select-xxx methiod', () => {
const { getByTestId, getByRole: getByRoleParent } = component
const element = getByTestId('select-xxx');
const { getByRole } = within(element)
const select = getByRole('button')
fireEvent.mouseDown(select);
const list = within(getByRoleParent('listbox')) // get list opened by trigger fireEvent
fireEvent.click(list.getByText(/just try/i)); //select by text
})
这就是我在使用 MUI 5 时的工作方式。
userEvent.click(screen.getByLabelText(/^foo/i));
userEvent.click(screen.getByRole('option', {name: /^bar/i}));
对于有多个选择的人,请确保添加 name
属性
<SelectDropdown
name="date_range"
...
>
...
</SelectDropdown>
<SelectDropdown
name="company"
...
>
...
</SelectDropdown>
// date filter
const date_range_dropdown = getByLabelText('Date Range');
fireEvent.mouseDown(date_range_dropdown);
await screen.findByRole('listbox');
fireEvent.click(
within(screen.getByRole('listbox')).getByText(/Last 30 Days/)
);
// // company filter
const company_dropdown = getByLabelText('Company');
fireEvent.mouseDown(company_dropdown);
fireEvent.click(within(getByRole('listbox')).getByText(/Uber/));