Tom Bogle
Tom Bogle

Reputation: 509

How to get shadcn/radix SelectContent options to render (on portal) when select is triggered in react testing library

I have a component that includes a shadcn Select control. The SelectTrigger component has role: 'combobox', but in the generated HTML, it is implemented as an HTML button. The component works perfectly in the browser, but I'd like to have a unit test that checks to ensure that the five expected options are actually displayed in the correct order and with the expected values. The unit test was super straightforward and passed beautifully when the implementation used an HTML select element because the options were actually rendered in the DOM even when the combobox was not dropped down. But now that I've changed it to be based on shadcn's Select components, the contents are rendered in a portal and only appear in the DOM when the list is being displayed. Here is a stripped down version of the component:

import '@/components/scripture-ref-keyed-list.component.css';
import { ScriptureCheckDefinition, ScriptureItemDetail } from 'platform-bible-utils';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from './shadcn-ui/select';

export type ScriptureSrcItemDetail = ScriptureItemDetail & {
  /** Source/type of detail. Can be used for grouping. */
  source: string | ScriptureCheckDefinition;
};

const scrBookColId = 'scrBook';
const typeColId = 'source';

export default function ScriptureRefKeyedList() {
  // Define possible grouping options
  const scrBookGroupName = 'one';
  const typeGroupName = 'two';

  const groupingOptions = [
    { label: 'No Grouping', value: [] },
    { label: `Group by ${scrBookGroupName}`, value: [scrBookColId] },
    { label: `Group by ${typeGroupName}`, value: [typeColId] },
    { label: `Group by ${scrBookGroupName} and ${typeGroupName}`, value: [scrBookColId, typeColId], },
    { label: `Group by ${typeGroupName} and ${scrBookGroupName}`, value: [typeColId, scrBookColId], },
  ];

  const handleSelectChange = (selectedGrouping: string) => {
    console.log(selectedGrouping);
  };

  return (
    <Select value={JSON.stringify([])} onValueChange={(value) => { handleSelectChange(value); }} >
      <SelectTrigger className="pr-mb-1">
        <SelectValue />
      </SelectTrigger>
      <SelectContent position="item-aligned">
        <SelectGroup>
          {groupingOptions.map((option) => (
            <SelectItem key={option.label} value={JSON.stringify(option.value)}>
              {option.label}
            </SelectItem>
          ))}
        </SelectGroup>
      </SelectContent>
    </Select>
  );
}

And here is my unit test:

import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import ScriptureRefKeyedList from '@/components/scripture-ref-keyed-list.component';

describe('ScriptureRefKeyedList default display mode (with combobox for grouping option)', () => {
  beforeEach(() => { render(<ScriptureRefKeyedList />); });

  it('should show grouping drop-down when clicked', async () => {
    const dropDown = screen.getByRole('combobox');
    // Check that No Grouping is the current (default) option:
    expect(screen.getByText('No Grouping')).toBeInTheDocument();

    expect(fireEvent.click(dropDown)).toBeTruthy();

    await waitFor(() => {
      expect(screen.findByText('Group by one')).toBeInTheDocument();
    });
  });
});

I have tried both fireEvent and userEvent to fire different events (that work in the browser) to get the list to render:

I have also tried different ways to get the elements:

And most importantly, I have scoured Stack Overflow and the Web and tried every idea I can find to wait for the DOM to re-render with the portal containing the options:

But the bottom line is that no matter what I try, the DOM never gets updated in the unit test, and the attempt to find or get any of the options fails (except, of course, getting "No Grouping" by text, since that is the default option and does appear in the DOM). I see that lots of other people have had similar problems. In a few cases, it seems the suggested solutions worked for some people, but after trying all the ideas I've seen, I'm still stymied.

Upvotes: 1

Views: 1393

Answers (2)

Antoni Xu
Antoni Xu

Reputation: 127

if you take a look at the radix-ui primitive code here, it have three functions that could alter the select context content value (a boolean to toggle content visibility), onClick, onPointerDown, onKeyDown. I think all works well if you use cypress, but only one function works on jest which is onKeyDown, you could try code below:

import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import ScriptureRefKeyedList from '@/components/scripture-ref-keyed-list.component';

describe('ScriptureRefKeyedList default display mode (with combobox for grouping option)', () => {
  beforeEach(() => { render(<ScriptureRefKeyedList />); });

  it('should show grouping drop-down when clicked', () => {
    const dropDown = screen.getByRole('combobox');
    const keyDownEvent = new KeyboardEvent("keydown", {
                             bubbles: true,
                             cancelable: true,
                             key: "Enter",
                             keyCode: 13
                         });
    myDropdown.dispatchEvent(keyDownEvent);

    expect(screen.findByText('Group by one')).toBeInTheDocument();
  });
});

Upvotes: -1

Jasperan
Jasperan

Reputation: 3806

You need to use fireEvent.pointerDown to open up the select options in your unit test. You'll also need to mock a few things. I'm using vitest but you can use jest as well.

// required mocks to open Shadcn Select component
export class MockPointerEvent extends Event {
  button: number;
  ctrlKey: boolean;

  constructor(type, props) {
    super(type, props);
    if (props.button != null) {
      this.button = props.button;
    }
    if (props.ctrlKey != null) {
      this.ctrlKey = props.ctrlKey;
    }
  }
}
window.PointerEvent = MockPointerEvent as any;
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
window.HTMLElement.prototype.releasePointerCapture = vi.fn();

Then in your test code, use

fireEvent.pointerDown(
  selectElementTrigger,
  new MockPointerEvent('pointerdown', {
    ctrlKey: false,
    button: 0,
  })
);

For a full test example, see this gist I made.

Upvotes: 0

Related Questions