primitives: [Select] Unable to open select with @testing-library/react

Bug report

Current Behavior

The library cannot open the select when writing unit tests with @testing-library/react against the Select component. This happens in all three cases:

Expected behavior

The Select component can be opened in jest unit tests.

Reproducible example

https://codesandbox.io/s/elated-gagarin-18cpyl?file=/select.test.jsx

Suggested solution

Additional context

When using fireEvent.click, the Select component simply doesn’t open, and the testing library can’t find any of the items. In other cases, an exception is thrown: The provided value is not of type 'Element'..

Your environment

Software Name(s) Version
Radix Package(s) react-select 1.1.2
React n/a 18.2.0
Browser n/a n/a
Assistive tech n/a n/a
Node n/a
npm/yarn
Operating System macOS Monterey

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 20
  • Comments: 16 (2 by maintainers)

Most upvoted comments

Stand alone React App

I offer you my jest testing file for a custom component that incorporates the radix-ui Select component

package.json excerpt

{
"dependencies": {
"@radix-ui/react-select": "^1.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^27.0.1",
"@types/react": "^18.0.0",
}

setupTests.ts

This file can be copy pasted into the test file if you are not using a test runner.

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

/**
 * JSDOM doesn't implement PointerEvent so we need to mock our own implementation
 * Default to mouse left click interaction
 * https://github.com/radix-ui/primitives/issues/1822
 * https://github.com/jsdom/jsdom/pull/2666
 */
class MockPointerEvent extends Event {
  button: number;
  ctrlKey: boolean;
  pointerType: string;

  constructor(type: string, props: PointerEventInit) {
    super(type, props);
    this.button = props.button || 0;
    this.ctrlKey = props.ctrlKey || false;
    this.pointerType = props.pointerType || 'mouse';
  }
}

window.PointerEvent = MockPointerEvent as any;
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.releasePointerCapture = jest.fn();
window.HTMLElement.prototype.hasPointerCapture = jest.fn();

select-component.test.tsx

// Test for SelectComponent component
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import SelectComponent from '@components/SelectComponent';

const mockOnChange = jest.fn();

describe('SelectComponent', () => {
  afterEach(() => {
    cleanup();
    jest.clearAllMocks();
  });
  it('should render', () => {
    render(
      <SelectComponent value={'option-1'} onChange={mockOnChange} />
    );
    expect(screen.getByText('option-1')).toBeInTheDocument();
  });

  it('should render placeholder', () => {
    render(
      <SelectComponent placeholder="Select an option" onChange={mockOnChange} />
    );
    // This should match your placeholder default if you dont pass one in
    expect(screen.getByText('Select an option')).toBeInTheDocument();
  });

  it('should render options', async () => {
    const user = userEvent.setup();
    render(
      <SelectComponent value={'option-2'} onChange={mockOnChange} />
    );
    // Target the combo box by name instead of the text inside of it in order to click it
    const optionSelect = screen.getByRole('combobox', {
      name: 'Select an option',
    });
    expect(optionSelect).toBeInTheDocument();
    await user.click(optionSelect);

    expect(
      screen.getByRole('option', { name: 'option-1' })
    ).toBeInTheDocument();
    expect(
      screen.getByRole('option', { name: 'option-2' })
    ).toBeInTheDocument();
    // Add in however many options you have
  });

  it('should render disabled options', async () => {
    const user = userEvent.setup();
    render(
      <SelectComponent
        value={'option-2'}
        onChange={mockOnChange}
        // Callback to determine if an option should be disabled
        isDisabled={(option) =>
          option === 'option-2' 
        }
      />
    );
    const optionSelect = screen.getByRole('combobox', {
      name: 'Select an option',
    });
    expect(optionSelect).toBeInTheDocument();
    await user.click(optionSelect);
    const option2 = screen.getByRole('option', { name: 'option-2' });
    expect(option2).toHaveAttribute('aria-disabled', 'true');
  });

  it('should call onChange', async () => {
    const user = userEvent.setup();
    render(
      <SelectComponent value={'option-1'} onChange={mockOnChange} />
    );
    const optionSelect = screen.getByRole('combobox', {
      name: 'Select an option',
    });
    expect(optionSelect).toBeInTheDocument();
    await user.click(optionSelect);
    const option2 = screen.getByRole('option', { name: 'option-2' });
    expect(option2).toBeInTheDocument();
    await user.click(option2);
    expect(mockOnChange).toHaveBeenCalledWith('option-2');
  });
});

The issue for me it seems was that targeting the userEvent to a getByText was being blocked by that element’s pointer-events: none CSS. So using a getByRole target seems to do the trick to get the combobox

My tests worked for a long time with the mock specified in this ticket: https://github.com/radix-ui/primitives/issues/1382#issuecomment-1122069313

Since the last version (1.1.2) and the introduction of this fix https://github.com/radix-ui/primitives/pull/1753, I had this error: TypeError: target.hasPointerCapture is not a function

So I fixed it by adding this line in the setupTest.ts file:

window.HTMLElement.prototype.hasPointerCapture = jest.fn();

So here is the complete test configuration to use the Select primitive with testing-library:

/**
 * JSDOM doesn't implement PointerEvent so we need to mock our own implementation
 * Default to mouse left click interaction
 * https://github.com/radix-ui/primitives/issues/1822
 * https://github.com/jsdom/jsdom/pull/2666
 */
class MockPointerEvent extends Event {
  button: number;
  ctrlKey: boolean;
  pointerType: string;

  constructor(type: string, props: PointerEventInit) {
    super(type, props);
    this.button = props.button || 0;
    this.ctrlKey = props.ctrlKey || false;
    this.pointerType = props.pointerType || 'mouse';
  }
}

window.PointerEvent = MockPointerEvent as any;
window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.releasePointerCapture = jest.fn();
window.HTMLElement.prototype.hasPointerCapture = jest.fn();

Here is the solution without any mocking:

const element = screen.getByTestId(...);
await userEvent.click(element, { pointerState: await userEvent.pointer({ target: element }) });

Here is a setup with vitest and it seems to work. I hope it helps.

> const element = screen.getByTestId(...);
> await userEvent.click(element, { pointerState: await userEvent.pointer({ target: element }) });

don’t work for me

this is my code

<SelectTrigger className='w-full focus:ring-1 focus:ring-gray-300' data-testid= 'select-style-to-edit'>
   <SelectValue placeholder='Select style to edit' />
</SelectTrigger>
const element = screen.getByTestId('select-style-to-edit')
await userEvent.click(element, {pointerState: await userEvent.pointer({ target: element }),})

TypeError: target.hasPointerCapture is not a function. (In 'target.hasPointerCapture(event.pointerId)', 'target.hasPointerCapture' is undefined)

Same, the solution doesn’t work for me either. Using vitest and happy-dom.

FWIW, I was accidentally doing userEvent.click() instead of await userEvent.click(). Awaiting fixed the problem.

> const element = screen.getByTestId(...);
> await userEvent.click(element, { pointerState: await userEvent.pointer({ target: element }) });

don’t work for me

this is my code

<SelectTrigger className='w-full focus:ring-1 focus:ring-gray-300' data-testid= 'select-style-to-edit'>
   <SelectValue placeholder='Select style to edit' />
</SelectTrigger>
const element = screen.getByTestId('select-style-to-edit')
await userEvent.click(element, {pointerState: await userEvent.pointer({ target: element }),})

TypeError: target.hasPointerCapture is not a function. (In 'target.hasPointerCapture(event.pointerId)', 'target.hasPointerCapture' is undefined)

Same