对 Material UI Select 组件的更改做出反应测试库

Posted

技术标签:

【中文标题】对 Material UI Select 组件的更改做出反应测试库【英文标题】:React testing library on change for Material UI Select component 【发布时间】:2019-08-06 14:49:57 【问题描述】:

我正在尝试使用react-testing-library 测试Select component 的onChange 事件。

我使用getByTestId 抓取元素,效果很好,然后设置元素的值,然后调用fireEvent.change(select);,但永远不会调用onChange,并且永远不会更新状态。

我尝试过使用 select 组件本身以及获取对底层 input 元素的引用,但都不起作用。

有什么解决办法吗?或者这是一个已知问题?

【问题讨论】:

【参考方案1】:

material-ui 的 select 组件使用 mouseDown 事件触发弹出菜单出现。如果您使用 fireEvent.mouseDown 应该触发弹出窗口,然后您可以在出现的列表框中单击您的选择。请参阅下面的示例。

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";

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);
);

【讨论】:

是的,这是测试它的正确方法。您可以通过检查 material-ui 如何测试其组件github.com/mui-org/material-ui/blob/master/packages/material-ui/… 来获取更多详细信息 如果我有多个 @YaserAliPeedikakkal 如果您的Select 有标签,您可以使用getByLabelText() 定位Select 进行首次点击。带有role="listbox" 的元素会在点击后出现,因此除非您自己添加了带有role="listbox" 的元素,否则下一个查询只会从您的目标点击中找到第一个弹出窗口。例如,user-eventuserEvent.click(getByLabelText("Select Label")); userEvent.click(within(getByRole("listbox")).getByText("Option Text")); @Kentr 设置标签并单击标签将不起作用,因为标签仅应用于父 div,单击它不会触发弹出窗口打开。 @AswinPrasad 下面是一个使用标签的工作示例:codesandbox。测试没有在代码沙盒上运行,可能是由于我的错误。测试确实在我的计算机上运行(并通过)。【参考方案2】:

当您使用 Material-UI 的 Selectnative=false(这是默认设置)时,这会变得非常复杂。这是因为渲染的输入甚至没有&lt;select&gt; html 元素,而是混合了 div、隐藏输入和一些 svg。然后,当您单击选择时,会显示一个表示层(有点像模式),其中包含您的所有选项(顺便说一下,这些选项不是&lt;option&gt; HTML 元素),我相信这是单击其中一个这些选项会触发您作为 onChange 回调传递给原始 Material-UI &lt;Select&gt; 的任何内容@

话虽如此,如果您愿意使用&lt;Select native=true&gt;,那么您将拥有实际的&lt;select&gt;&lt;option&gt; HTML 元素可以使用,并且您可以在&lt;select&gt; 上触发更改事件你会预料到的。

这是来自代码沙箱的测试代码,可以运行:

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 渲染出其 &lt;Select&gt;,您必须深入节点才能找到实际的 &lt;select&gt; 所在的位置。但是一旦你找到它,你可以在上面做一个fireEvent.change

CodeSandbox 可以在这里找到:

【讨论】:

感谢@Alvin Lee,这正是我们所需要的。为了将来参考,我们在 inputProps 中设置了测试 ID,因此:inputProps= "data-testid": "my-wrapper" ,然后不必通过引用 2 个子节点来获取选择节点。 @RobSanders 很高兴它为你解决了!这是关于设置测试 ID 而不是深入挖掘子节点的有用提示。编码愉快!【参考方案3】:

这是一个带有 Select 选项的 MUI TextField 的工作示例。

沙盒:https://codesandbox.io/s/stupefied-chandrasekhar-vq2x0?file=/src/__tests__/TextSelect.test.tsx:0-1668

文本字段:

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
 *
 *
 */

【讨论】:

【参考方案4】:
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();

    );
);

【讨论】:

我们可能会走这条路,但我会尽量避免使用 TesUtils.Simulate,因为它不是真实事件,因此也不是我们可以做的最真实的测试。【参考方案5】:
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);

);

【讨论】:

【参考方案6】:

我在使用 Material UI 选择元素时遇到了一些问题,但最后我找到了这个简单的解决方案。

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)
    )
  )
)

【讨论】:

【参考方案7】:

使用*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上运行,但在本地运行并通过。

【讨论】:

以上是关于对 Material UI Select 组件的更改做出反应测试库的主要内容,如果未能解决你的问题,请参考以下文章

在 Material UI Select 组件中搜索输入作为选项

酶不模拟 React Material-UI v1 上的更改事件 - 选择组件

如何使用 Material-Ui Autocomplete for Multi-Select 复选框实现 Formik 的 Field 组件?

Material UI Select Component - 一个组件正在将文本类型的受控输入更改为不受控制

React Material-UI Select未检测到滚动事件

Material-UI 和 Redux-form,点击 select 时重新渲染选项,如何防止?