user-event: Regression: Click event suddenly stopped working (Reactstrap / Popper.js 1.x)

  • @testing-library/user-event version: 13.1.1
  • Testing Framework and version: Jest 26.6.3.
  • DOM Environment: JSDOM 16.4.0, node 12.19.0

Relevant code or config

This code opens a modal that contains some buttons and clicks on them. The modal is provided by Reactstrap (8.9.0).

    it('can discard and apply a filter selection', () => {
        render(
            <TestRootContainer initialState={mockStore}>
                <BookingsFilter />
            </TestRootContainer>,
        );

        // open filter popover
        user.click(
            screen.getByRole('button', {
                name: 'cmb-web.filter.filter-by',
            }),
        );

        // expect filter apply button
        screen.getByRole('button', { name: 'cmb-web.filter.apply' });
        // expect no filters set
        screen.getByText('cmb-ui-core.active-filter-items.no-active-filters');
        // expect no reset button
        expect(screen.queryByRole('button', { name: 'cmb-web.filter.reset' })).toBe(null);

        // select a byType filter
        user.click(screen.getByRole('button', { name: 'cmb-ui-core.account-detail-filter.credit' }));
       //^^^^^^^^^^^^^
       //Here the code execution just stops. The onclick handler on the button is never called.

        // The test failed here because the button does not appear.
        user.click(screen.getByRole('button', { name: 'cmb-web.filter.reset' }));


        // Expect popover to have closed
        expect(screen.queryByRole('button', { name: 'cmb-web.filter.apply' })).toBe(null);

What you did: This test failed after an upgrade of user-events from 12.6.3 to 13.1.1. It worked again when I downgraded back to 12.6.3.

What happened: The test failed at the marked spot. Adding async / await did not help. Also using the bound getByRole did not help.

When run with the debugger the context never switched back to the React component after the click on the button. The flow went straight to the next line in the test.

The test then failed in the next line since the button click handler had not been called the UI never updated.

Reproduction repository:

I’ll try to find time to create a minimal repo that can reproduce the issue. But I hope that maybe you have an idea what could be the reason for this regression.

Problem description: The modal component does register some event listeners on document that close it when the user clicked outside of it. Could this be the reason?

If it helps, I can go through every release in between and try to see where the regression happened.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 4
  • Comments: 33 (19 by maintainers)

Most upvoted comments

If somebody still has this problem skipPointerEventsCheck option was introduced and can be used in the following way:

userEvent.click(await screen.findByText('Zoom'), {}, { skipPointerEventsCheck: true });

Cool! I’ll try get something up next week 😃

Workaround for version 14.0.0+:

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

test('workaround for pointer-events caching', async () => {
  // keep helpful pointer-event checks enabled until
  // they start causing errors (e.g. with popper.js + react-bootstrap)
  const user = userEvent.setup();

  // click a button with label 'Open Menu'
  // that opens a menu div with transition
  await user.click(screen.getByText('Open Menu'));

  // wait for div[role=menu] to be interactive
  await waitFor(() => {
    // do a manual check that confirms the popup
    // actually removes its pointer-events style
    expect(
      window.getComputedStyle(screen.getByRole('menu')).pointerEvents
    ).not.toBe('none');
  });

  // create a sub-instance of userEvent, disabling pointerEventsCheck,
  // then click a menu item
  await user
    .setup({ pointerEventsCheck: PointerEventsCheckLevel.Never })
    .click(await screen.findByText('Menu Item'));
});

@ph-fritsche I’ve also experienced this issue, and even though I fully understand the rationale behind this decision (and think it makes sense to consider no-pointer-events as being non-clickable), I think we could perhaps improve this behaviour.

Similar to how testing-library has the getBy query which will error if the element is not found, and the convenient findBy ( / waitFor) helpers to retry until the element is found, could it make sense to add a behaviour which would throw an error if we try to click a non-clickable element? (i.e. disabled, or no pointer events). (or at a minimum, a console.warning when this occurs)

This would really ease the DX for people dealing with external libraries where you can’t easily control this behaviour, animated components, and as well make it way more clear why tests are failing. Right now, it’ll just ‘no-op’ if the element is not clickable, giving the responsibility of raising an error to the next assertion, and thus easily making the cause of the test failure quite obscure.

This would also make it way easier to work around when you have animating components or inflexible libraries: you’d then simply have to wrap your click in

await waitFor(() => userEvents.click(screen.getByRole('button')))

Please let me know what you think and if this deserves its own ticket. I’d also be happy to try contribute this to the project if this is something you’d be interested in.

pointer-events is auto.

@vicrep Don’t get me wrong. If you feel strongly about this, a feature request in a separate issue is welcome. It’s just that at the moment I fear it brings a couple of new problems while not really solving one. If you come up with a consistent policy for such a flag that we could implement across our APIs and it receives positive feedback, I would consider an implementation.

@ph-fritsche I was thinking, could we consider adding a force option to user events, which bypasses interactivity checks? That way people can still use library for user events instead of falling back to the inferior fireEvent.* , or inconsistently using one or the other. (Again, happy to contribute)

Something like

userEvent.click(element, null, { force: true })

It’s nice to have errors if elements can’t be clicked. But I can’t solve the initial issue.

My issue was solved by waiting for popper to rerender with the correct styles

I tried a number of different ways to do this:

  • doing a simple timeout, ranging from 0 to 1000ms
  • overwriting pointer-events with auto at the point where I think it must be
  •   await waitFor(() =>
        expect(
          hasPointerEvents(container.querySelector('bp3-transition-container')),
        ).toBe(true),
      );
    
  •       await waitFor(() =>
        expect(
          userEvent.click(screen.getByLabelText('Tue Dec 10, 2019')),
        ).not.toThrow(),
      );
    

As of now I sadly can’t upgrade the project away from popper v1, which is used by blueprint.

In the final application the pointer events are set to auto as expected.

I’m pretty much stuck here, the only way I know which “solves” this is by using fireEvent.click instead

@markivancho how did you solve this?

I am facing the same issue with v13.1.x. I am using react-popper v1.3.6

We’ve added support for pointer-events in v13.1.0.

Could you check the computed pointer-events value for the element that you’re trying to click? Per window.getComputedStyle(element)['pointer-events'] and through developer console of your browser.

It returns auto in the browser’s developer console but returns none when I log it in the test file.

As mentioned by @markivancho , react-popper adds pointer-events: none as initial style to the popper element. Then the element re-renders and updates the value to auto. This creates a race-condition as when the click event is triggered, the value of pointer-events is still none

Adding a waiting time of “0 ms” resolves this issue. await new Promise(res => setTimeout(() => res(), 0))

But, it’s not feasible to add this workaround for multiple failing tests.