React 测试库对 Material UI Select 组件的更改

React testing library on change for Material UI Select component

我正在尝试测试 Select component using react-testing-libraryonChange 事件。

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

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

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

当您将 Material-UI 的 Selectnative={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 的工作示例。

沙盒: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
 *
 *
 */

我在使用 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/));