Lessons learned from upgrading testing-lib's user-event to v14 in Kibana
/ 4 min read
Table of Contents
Kibana is among the largest public monorepos on Github with 3+ millions lines of TypeScript, 150+ monthly committers.
Maintaining this codebase comes with its own challenges. One of them is to keep third party dependencies up to date. Last year (I wanted to write about this for a while …), I took on upgrading @testing-library/user-event
from version 13 to the then latest v14.5.2, it turned out to be quite a rabbit hole!
This wasn’t a small PR: The upgrade affected 401 files (5695 inserts, 4719 deletions) and it required reviews from 20+ CODEOWNER teams! user-event
version 14 was already released back in March 2022, but the update wasn’t merged to Kibana until September 2024, tech debt knocking! So this is definitely not a blog post about the newest/fanciest of things, more like acknowledging that tech debt can be of course be real in large codebases.
Breaking Changes
Version 14 of user-event came with changes in how events are handled. The most impactful changes were:
-
user-event methods now return Promises - Every
userEvent.click()
,userEvent.type()
, etc. now needs to be awaited. -
Pointer event handling changed - The old pattern of using
{ skipPointerEventsCheck: true }
needed to be replaced with{ pointerEventsCheck: 0 }
. -
Event bubbling and propagation - The new version more closely resembles real browser behavior, which uncovered some hidden bugs in our tests.
Performance Regressions
One unexpected challenge was performance degradation. Our test suite became noticeably slower after the upgrade and hitting timeouts became a problem, particularly with typing operations. That’s not just a Kibana-specific issue – there are some reports of similar issues with v14.
We implemented several workarounds:
// Optimizing typing performanceuserEvent.setup({ delay: null }); // Disable artificial delay
// For pointer event checksuserEvent.click(element, { pointerEventsCheck: 0 }); // Skip expensive DOM traversal
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
In other cases we had to defer to fireEvent
.
The maintainers have made it clear that simulating realistic user behavior takes precedence over test speed for them. While this is philosophically sound, it creates practical challenges for large test suites with timeouts. In our case, we had to increase some test timeouts and reconsider our testing approach for some form-heavy components. Regressions like this are also a cost factor in large organizations since it will increase the time spent on CI runs.
Implementation Challenges
In a codebase with thousands of tests, these changes were non-trivial. I initially hoped to use a codemod for most of the conversion, but the edge cases proved too numerous and context-dependent, esp. around custom test harnesses.
Some patterns that needed updating:
// BeforeuserEvent.click(element);expect(something).toBe(true);
// Afterawait userEvent.click(element);expect(something).toBe(true);
And for pointer event checks:
// BeforeuserEvent.click(element, undefined, { skipPointerEventsCheck: true });
// AfteruserEvent.click(element, { pointerEventsCheck: 0 });
Change to check if an element was removed:
// Beforeawait waitForElementToBeRemoved(() => screen.getByText('Learn React'));
// Afterawait waitFor(() => { expect(screen.queryByText('Learn React')).not.toBeInTheDocument();});
Keyboard events:
// BeforeuserEvent.keyboard('{arrowright}');
// Afterawait user.keyboard('[ArrowRight]');
Migrate away from act
:
// Beforeact(() => { userEvent.type( screen.getByTestId('some-id'), 'some text' );});
// Afterawait userEvent.type( screen.getByTestId('some-id'), 'some text');
The API for userEvent.paste
changed:
// BeforeuserEvent.paste(await screen.findByTestId('some-id'), 'some text');
// Afterawait userEvent.click(await screen.findByTestId('some-id'));await userEvent.paste('some text');
Lessons Learned
This upgrade was essentially an archaeological dig through years of test patterns of different teams, it was a great learning experience. Doing the whole thing was worth the pain, it was highly appreciated by other developers. Note that all of this was done manually since back then we didn’t have any approval to use fully agentic LLM coding workflows. Not sure how many projects out there are still on v13, hope this summary is still useful for some devs.
Check out the full PR on GitHub if you’re interested in the gory details.