例子

在线地址:https://codesandbox.io/p/github/18888628835/recoil-todo-list-with-test/main

image-20221113144256984

断言 input 能够输入

js
1/* 
2<input type="text" value={inputValue} onChange={onChange} />;
3*/
4it('input can be typed', () => {
5  render(<TodoItemCreator />)
6  const inputEl = screen.getByRole('textbox')
7  userEvent.type(inputEl, 'type some text ')
8  expect(inputEl.getAttribute('value')).toBe('type some text ')
9  userEvent.type(inputEl, 'continue')
10  expect(inputEl.getAttribute('value')).not.toBe('continue')
11  expect(screen.getByDisplayValue('type some text continue')).toBeInTheDocument()
12})

断言 select.option 正常渲染

js
1/**
2  const options = [
3    { label: 'All', value: 'Show All' },
4    { label: 'Completed', value: 'Show Completed' },
5    { label: 'Uncompleted', value: 'Show Uncompleted' },
6  ]; 
7  <select>
8  {options.map(({ label, value }, index) => (
9    <option key={index} value={value} data-testid="select-option">
10      {label}
11    </option>
12  ))}
13</select> 
14 */
15
16it('select options', () => {
17  render(<App />)
18  const options = screen.getAllByTestId('select-option')
19  expect(options[0].getAttribute('value')).toBe('Show All')
20  expect(options[1].getAttribute('value')).toBe('Show Completed')
21  expect(options[2].getAttribute('value')).toBe('Show Uncompleted')
22})

断言异步的 select.option 正常渲染

js
1/**
2 useEffect(() => {
3    const dataFromDB = [
4      { label: 'All', value: 'Show All', disabled: false },
5      { label: 'Completed', value: 'Show Completed', disabled: false },
6      { label: 'Uncompleted', value: 'Show Uncompleted', disabled: true },
7    ];
8    Promise.resolve(dataFromDB).then((res) => setOptions([...res]));
9  }, []);
10 */
11it('async get select options', async () => {
12  render(<App />)
13  const options = await screen.findAllByTestId('select-option')
14  expect(options[0].getAttribute('value')).toBe('Show All')
15  expect(options[1].getAttribute('value')).toBe('Show Completed')
16  expect(options[2].getAttribute('value')).toBe('Show Uncompleted')
17})

断言 select.option 有默认值

js
1it('default select-option', async () => {
2  render(<App />);
3  const options = await screen.findAllByTestId('select-option');
4  expect((options[0] as HTMLOptionElement).selected).toBeTruthy();
5  expect((options[1] as HTMLOptionElement).selected).toBeFalsy();
6  expect((options[2] as HTMLOptionElement).selected).toBeFalsy();
7});

断言 select.option 能被选择

js
1it('select-option selected', async () => {
2  render(<App />);
3  const selectEl = await screen.findByRole('combobox');
4  const options = await screen.findAllByTestId('select-option');
5  userEvent.selectOptions(selectEl, options[1]);
6  expect((options[0] as HTMLOptionElement).selected).toBeFalsy();
7  expect((options[1] as HTMLOptionElement).selected).toBeTruthy();
8  expect((options[2] as HTMLOptionElement).selected).toBeFalsy();
9});

断言 select.option 不能被选择

js
1it('one select-option is disabled', async () => {
2  render(<App />)
3  const options = await screen.findAllByTestId('select-option')
4  // The following code will report an error
5  // userEvent.selectOptions(selectEl, options[3]);
6  expect(options[2]).toBeDisabled()
7})

断言 UL-list 正常渲染

js
1/**
2    <ul>
3      <li>Total items: {totalNum}</li>
4      <li>Items completed: {totalCompletedNum}</li>
5      <li>Items not completed: {totalUncompletedNum}</li>
6      <li>Percent completed: {formattedPercentCompleted}</li>
7    </ul>
8 */
9it('should render list of 4 list-items', async () => {
10  render(<App />)
11  await waitFor(() => {
12    const list = screen.getByRole('list')
13    const { getAllByRole } = within(list)
14    const items = getAllByRole('listitem')
15    expect(items.length).toBe(4)
16  })
17})

断言匹配 snapshot

js
1it('match snapshot', async () => {
2  const { asFragment } = render(<App />)
3  await screen.findAllByTestId('select-option')
4  expect(asFragment()).toMatchSnapshot()
5})

由于例子中的 select-option 是异步获取的,如果不写 await ...则会报act(...)

断言能够增加一行

js
1it('add Item', async () => {
2  render(<App />)
3  const inputEl = screen.getByRole('textbox')
4  const buttonEl = screen.getByRole('button', { name: /add/i })
5  userEvent.type(inputEl, 'item1')
6  userEvent.click(buttonEl)
7  await act(async () => Promise.resolve())
8  expect(await screen.findByDisplayValue('item1')).toBeInTheDocument()
9  expect(screen.queryByDisplayValue('item2')).toBeNull()
10  expect(screen.queryByDisplayValue('item3')).not.toBeInTheDocument()
11})

同样的原因:await act(async () => Promise.resolve())为了解决异步获取 select-option导致的 TLR报的 warning

断言能够删除一行

js
1it('delete Item', async () => {
2  render(<App />)
3  const inputEl = screen.getByRole('textbox')
4  const buttonEl = screen.getByRole('button', { name: /add/i })
5  userEvent.type(inputEl, 'item1')
6  userEvent.click(buttonEl)
7  await act(() => Promise.resolve())
8  const todoItem = screen.getByTestId('todo-item-0')
9  const { findByRole } = within(todoItem)
10  const deleteEl = await findByRole('button', { name: /x/i })
11  userEvent.click(deleteEl)
12  expect(screen.queryByDisplayValue('item1')).toBeNull()
13  // expect(await screen.findByDisplayValue('item1')).toBeInTheDocument();
14})

测试 Recoil-state

首先需要利用 RecoilObserver 来监听 Recoil 数据

js
1// @ts-ignore
2const RecoilObserver = ({ state, onChange }) => {
3  const value = useRecoilValue(state)
4  useEffect(() => onChange(value), [onChange, value])
5  return null
6}

state 增加和删除

js
1it('be change when user click add button and x button', async () => {
2  const onChange = jest.fn()
3  render(
4    <RecoilRoot>
5      <RecoilObserver state={todoListState} onChange={onChange} />
6      <TodoList />
7    </RecoilRoot>,
8  )
9  const inputEl = await screen.findByRole('textbox')
10  const buttonEl = await screen.findByRole('button', { name: /add/i })
11  // the following code will report error: act(...)
12  //const inputEl = screen.getByRole('textbox');
13  //const buttonEl = screen.getByRole('button', { name: /add/i });
14  userEvent.type(inputEl, 'item1')
15  userEvent.click(buttonEl)
16  expect(onChange).toHaveBeenCalledTimes(2)
17  expect(onChange).toHaveBeenCalledWith([])
18  expect(onChange).toHaveBeenCalledWith([{ id: expect.any(Number), isComplete: false, text: 'item1' }])
19
20  const todoItem = screen.getByTestId('todo-item-0')
21  const { findByRole } = within(todoItem)
22  const deleteEl = await findByRole('button', { name: /x/i })
23  userEvent.click(deleteEl)
24  expect(onChange).toHaveBeenCalledWith([])
25})

state 异步查询

js
1  it('async get data from backend', async () => {
2    const mockState = { login: 'test title' };
3    global.fetch = jest.fn(() =>
4      Promise.resolve({
5        json: () => Promise.resolve(mockState),
6      })
7    ) as jest.Mock;
8
9    render(
10      <RecoilRoot>
11        <Title />
12      </RecoilRoot>
13    );
14    expect(await screen.findByText(mockState.login)).toBeInTheDocument();
15  });

保存数据到数据库

js
1  it('save data', async () => {
2    global.fetch = jest.fn() as jest.Mock;
3    render(
4      <RecoilRoot>
5        <Save />
6      </RecoilRoot>
7    );
8    const buttonEl = screen.getByRole('button', { name: /Save/i });
9    userEvent.click(buttonEl);
10    expect(global.fetch).toBeCalledTimes(1);
11  });