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

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

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

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

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

【问题讨论】:

    标签: reactjs material-ui react-testing-library


    【解决方案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上运行,但在本地运行并通过。

          【讨论】:

            猜你喜欢
            • 2020-03-12
            • 2017-07-02
            • 2018-03-16
            • 1970-01-01
            • 2019-10-08
            • 1970-01-01
            • 2018-11-27
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多