MUI Selectテスト:基本から実践パターンまで網羅【React Testing Library】

MUI Selectのテスト 基本から実践パターンまで React

MUI SelectをReact Testing Libraryでテストする方法を解説します。

ドロップダウンの開閉、オプションの選択、初期値の確認、複数Selectの操作など、実務で必要なパターンを網羅しています。

動作環境

  • @mui/material v6
  • @testing-library/react v16
  • @testing-library/user-event v14
  • vitest v3

基本的なテスト

Selectの取得

MUI Selectはcomboboxロールを持ちます。getByLabelTextまたはgetByRole('combobox')で取得できます。

import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import {render, screen} from '@testing-library/react';

const BasicSelect = () => {
  const [value, setValue] = useState('');
  return (
    <FormControl fullWidth>
      <InputLabel id="category-label">カテゴリー</InputLabel>
      <Select
        labelId="category-label"
        label="カテゴリー"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      >
        <MenuItem value="electronics">電子機器</MenuItem>
        <MenuItem value="books">書籍</MenuItem>
        <MenuItem value="clothing">衣類</MenuItem>
      </Select>
    </FormControl>
  );
};

test('Selectがレンダリングされる', () => {
  render(<BasicSelect />);
  
  // getByLabelTextで取得
  const select = screen.getByLabelText('カテゴリー');
  expect(select).toBeInTheDocument();
  
  // getByRoleでも取得できる
  const combobox = screen.getByRole('combobox', {name: 'カテゴリー'});
  expect(combobox).toBeInTheDocument();
});

ドロップダウンの開閉

Selectをクリックするとドロップダウンが開きます。開いた状態はlistboxロールで確認できます。

import {waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('Selectをクリックするとドロップダウンが開く', async () => {
  const user = userEvent.setup();
  render(<BasicSelect />);

  const select = screen.getByLabelText('カテゴリー');
  await user.click(select);

  await waitFor(() => {
    expect(screen.getByRole('listbox')).toBeInTheDocument();
  });
});

test('ドロップダウンにすべてのオプションが表示される', async () => {
  const user = userEvent.setup();
  render(<BasicSelect />);

  await user.click(screen.getByLabelText('カテゴリー'));

  await waitFor(() => {
    expect(screen.getByRole('option', {name: '電子機器'})).toBeInTheDocument();
    expect(screen.getByRole('option', {name: '書籍'})).toBeInTheDocument();
    expect(screen.getByRole('option', {name: '衣類'})).toBeInTheDocument();
  });
});

オプションの選択

オプションはoptionロールで取得し、クリックで選択します。

test('オプションをクリックして選択できる', async () => {
  const user = userEvent.setup();
  render(<BasicSelect />);

  const select = screen.getByLabelText('カテゴリー');

  // ドロップダウンを開く
  await user.click(select);

  // オプションを選択
  await user.click(screen.getByRole('option', {name: '書籍'}));

  // 選択値が反映される
  expect(screen.getByRole('combobox', {name: 'カテゴリー'})).toHaveTextContent('書籍');
});

test('異なるオプションを選択すると値が更新される', async () => {
  const user = userEvent.setup();
  render(<BasicSelect />);

  const select = screen.getByLabelText('カテゴリー');

  // 1回目の選択
  await user.click(select);
  await user.click(screen.getByRole('option', {name: '書籍'}));
  expect(screen.getByRole('combobox', {name: 'カテゴリー'})).toHaveTextContent('書籍');

  // 2回目の選択(別のオプション)
  await user.click(select);
  await user.click(screen.getByRole('option', {name: '電子機器'}));
  expect(screen.getByRole('combobox', {name: 'カテゴリー'})).toHaveTextContent('電子機器');
});

初期値・選択値の確認

空の初期値(zero-width spaceの罠)

MUI Selectは空の状態でzero-width space(\u200B)を表示します。空かどうかを確認する際は注意が必要です。

test('初期値が空の場合、zero-width spaceが表示される', () => {
  render(<BasicSelect />);
  
  const combobox = screen.getByRole('combobox', {name: 'カテゴリー'});
  
  // 空の選択状態ではzero-width spaceのみ
  expect(combobox.textContent).toBe('\u200B');
  
  // toHaveTextContent('')は失敗する点に注意
});

初期値が設定されている場合

const SelectWithDefault = () => {
  const [value, setValue] = useState('books'); // 初期値あり
  return (
    <FormControl fullWidth>
      <InputLabel id="category-label">カテゴリー</InputLabel>
      <Select
        labelId="category-label"
        label="カテゴリー"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      >
        <MenuItem value="electronics">電子機器</MenuItem>
        <MenuItem value="books">書籍</MenuItem>
        <MenuItem value="clothing">衣類</MenuItem>
      </Select>
    </FormControl>
  );
};

test('初期値が設定されている場合、Selectに反映される', () => {
  render(<SelectWithDefault />);
  
  const combobox = screen.getByRole('combobox', {name: 'カテゴリー'});
  expect(combobox).toHaveTextContent('書籍');
});

選択されたオプションのaria-selected確認

test('選択されたオプションにaria-selectedが設定される', async () => {
  const user = userEvent.setup();
  render(<SelectWithDefault />); // 初期値: books

  await user.click(screen.getByLabelText('カテゴリー'));

  await waitFor(() => {
    const selectedOption = screen.getByRole('option', {name: '書籍'});
    expect(selectedOption).toHaveAttribute('aria-selected', 'true');
    
    const unselectedOption = screen.getByRole('option', {name: '電子機器'});
    expect(unselectedOption).toHaveAttribute('aria-selected', 'false');
  });
});

disabled状態のテスト

const DisabledSelect = ({disabled}: {disabled: boolean}) => {
  const [value, setValue] = useState('');
  return (
    <FormControl fullWidth disabled={disabled}>
      <InputLabel id="category-label">カテゴリー</InputLabel>
      <Select
        labelId="category-label"
        label="カテゴリー"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      >
        <MenuItem value="books">書籍</MenuItem>
      </Select>
    </FormControl>
  );
};

test('disabled=true でSelectが無効化される', () => {
  render(<DisabledSelect disabled={true} />);
  
  const combobox = screen.getByRole('combobox', {name: 'カテゴリー'});
  expect(combobox).toHaveAttribute('aria-disabled', 'true');
});

test('disabled=false でSelectが有効化される', () => {
  render(<DisabledSelect disabled={false} />);
  
  const combobox = screen.getByRole('combobox', {name: 'カテゴリー'});
  expect(combobox).not.toHaveAttribute('aria-disabled', 'true');
});

複数Selectの操作

複数のSelectがある場合、それぞれを独立して操作できます。

const MultipleSelectForm = () => {
  const [category, setCategory] = useState('');
  const [priority, setPriority] = useState('');

  return (
    <form>
      <FormControl fullWidth>
        <InputLabel id="category-label">カテゴリー</InputLabel>
        <Select
          labelId="category-label"
          label="カテゴリー"
          value={category}
          onChange={(e) => setCategory(e.target.value)}
        >
          <MenuItem value="electronics">電子機器</MenuItem>
          <MenuItem value="books">書籍</MenuItem>
        </Select>
      </FormControl>
      
      <FormControl fullWidth>
        <InputLabel id="priority-label">優先度</InputLabel>
        <Select
          labelId="priority-label"
          label="優先度"
          value={priority}
          onChange={(e) => setPriority(e.target.value)}
        >
          <MenuItem value="high">高</MenuItem>
          <MenuItem value="medium">中</MenuItem>
          <MenuItem value="low">低</MenuItem>
        </Select>
      </FormControl>
    </form>
  );
};

test('複数のSelectを独立して操作できる', async () => {
  const user = userEvent.setup();
  render(<MultipleSelectForm />);

  const categorySelect = screen.getByLabelText('カテゴリー');
  const prioritySelect = screen.getByLabelText('優先度');

  // カテゴリーを選択
  await user.click(categorySelect);
  await user.click(screen.getByRole('option', {name: '書籍'}));
  expect(screen.getByRole('combobox', {name: 'カテゴリー'})).toHaveTextContent('書籍');

  // 優先度を選択
  await user.click(prioritySelect);
  await user.click(screen.getByRole('option', {name: '高'}));
  expect(screen.getByRole('combobox', {name: '優先度'})).toHaveTextContent('高');

  // 両方の値が正しく保持されている
  expect(screen.getByRole('combobox', {name: 'カテゴリー'})).toHaveTextContent('書籍');
  expect(screen.getByRole('combobox', {name: '優先度'})).toHaveTextContent('高');
});

まとめ

この記事では、MUI SelectをReact Testing Libraryでテストする方法を解説しました。

テストパターン一覧

パターン使用するクエリ/マッチャー
Selectの取得getByLabelText() / getByRole('combobox')
ドロップダウン確認getByRole('listbox')
オプション取得getByRole('option', {name: '...'})
選択値の確認toHaveTextContent()
空の確認textContent === '\u200B'
disabled状態toHaveAttribute('aria-disabled', 'true')
選択状態toHaveAttribute('aria-selected', 'true')

ポイント

  1. MUI Selectはcomboboxロール、開いた状態はlistboxロールで取得
  2. オプションはoptionロールで取得し、クリックで選択
  3. 空の初期値はzero-width space(\u200B)になる点に注意
  4. disabled状態はaria-disabled属性で確認
  5. ドロップダウンの開閉は非同期なのでwaitForを使用

コメント

タイトルとURLをコピーしました