如何优雅地写拉框组合Combobox的单元测试

45 阅读1分钟

前言

现在很多组件库的Select组件都是基于combobox来开发的。由于combobox DOM结构复杂,即使是一个简单的操作,也需要多行代码才能在单元测试中模拟用户的操作。有没有办法用一行代码就能表达了?

一个简单的示例

test('应完成完整的选择流程', async () => {
  const user = userEvent.setup()
  render(<Combobox options={['Apple', 'Banana']} />)
  
  // 触发下拉
  await user.click(screen.getByRole('combobox'))
  
  // 验证选项渲染
  const options = await screen.findAllByRole('option')
  expect(options).toHaveLength(2)

  // 选择操作
  await user.click(options[1])
  expect(screen.getByRole('combobox')).toHaveValue('Banana')
})

解决方案

我们可以构造一个可以链式调用的class, 如下所示。

import { within } from '@testing-library/react';
import { ByRoleOptions, screen } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';

class Combobox {
  private dom: HTMLElement = document.createElement('div'); // default to a div dom

  test(dom: HTMLElement) {
    this.dom = dom;
  }

  getByLabelText(label: string) {
    this.dom = screen.getByLabelText(label);
    return this;
  }

  getByRole(role: string, options?: ByRoleOptions | undefined) {
    this.dom = screen.getByRole(role, options);
    return this;
  }

  getByTestId(testId: string) {
    this.dom = screen.getByTestId(testId);
    return this;
  }

  selectOption(text: string) {
    userEvent.click(this.dom);
    // Here assumes that the rendered dom will always have a single listbox
    const listbox = screen.getByRole('listbox');
    const option = within(listbox).getByText(text);
    expect(option).toBeInTheDocument();
    userEvent.click(option);
    return this;
  }

  isSelected(text: string) {
    expect(screen.getByText(text)).toBeInTheDocument();
    return this;
  }

  isNotSelected(text: string) {
    //list box is still there, which means option is not selected
    const listbox = screen.getByRole('listbox');
    expect(listbox).toBeInTheDocument();
    return this;
  }

  notExist(text: string) {
    userEvent.click(this.dom);
    // Here assumes that the rendered dom will always have a single listbox
    const listbox = screen.getByRole('listbox');
    const option = within(listbox).queryByText(text);
    expect(option).not.toBeInTheDocument();
    return this;
  }
}

const combobox = () => {
  return new Combobox();
};

export default combobox;

有了这个class之后,我们再来用一行代码重单元测试,如下所示:

test('应完成完整的选择流程', async () => {
  const user = userEvent.setup()
  render(<Combobox label='fruits' options={['Apple', 'Banana']} />)
  
  combobox().getByLabelText('fruits').selectOption('Banana').isSelected('Banana')
})