Skip to content

fix: create userEvent session lazily on first use - #265

Open
monsterlag wants to merge 1 commit into
guidepup:mainfrom
monsterlag:fix/issue-250-d953
Open

monsterlag wants to merge 1 commit into
guidepup:mainfrom
monsterlag:fix/issue-250-d953

Conversation

@monsterlag

Copy link
Copy Markdown
Contributor

Note

AI-generated, not reviewed by a human. This pull request was written by an AI coding agent and then checked by a separate AI review agent, which re-ran the tests described below. No human has reviewed the code. It is submitted by @monsterlag, who runs these agents. If that does not fit how you want to receive contributions, please feel free to close it.

Issue

Fixes #250.

Details

Since 0.33.0, virtual.start() always calls userEvent.setup(). That call prepares the document and the element that is focused at that moment (user-event installs value/selection interceptors on a focused <input>). When the test has its own, separate copy of @testing-library/user-event (for example one bundled by Storybook), the focused input ends up prepared twice, and later interactions from the test's own session (such as userEvent.clear(input)) no longer reach React's onChange, even for a walkthrough that only calls next() / lastSpokenPhrase().

This change follows the approach suggested in the issue:

  • start() now only stores the userEvent options (document, advanceTimers, defaults).
  • A private #getUserEvent() creates the session on first use, so act(), click(), press() and type() still share one persistent session for the walkthrough.
  • stop() clears both the session and the stored options, so the next start() gets a fresh session.
  • start() also discards any session left from an earlier start() without stop(), so a repeated start() still gets a fresh session with the new document / advanceTimers, as before.

Scope: this fixes walkthroughs that only navigate (start() / next() / previous() / lastSpokenPhrase() / stop()), which no longer touch userEvent at all, as in 0.32.1. A walkthrough that calls act(), click(), press() or type() still creates a userEvent session at that point, so with two copies of @testing-library/user-event it can still hit the original problem on an input focused at that moment.

A new test, test/int/userEventIsolation.int.test.tsx, loads a separate module instance of @testing-library/user-event with jest.isolateModules to simulate a second copy, types into a controlled React input, runs a start() / next() / stop() walkthrough, then clears the input and checks that React state updated.

A second new test in test/int/press.int.test.ts calls start() twice without stop(), with a different advanceTimers each time, and checks that press() after the second start() uses the second advanceTimers.

How it was tested (in a Linux container, Node 24, yarn 1.22):

  • yarn jest test/int/userEventIsolation.int.test.tsx without the src/Virtual.ts change: fails (Unable to find an element with the text: Current value: empty, the input's value changed but React's state did not).
  • The same command with the change: passes.
  • yarn jest test/int/press.int.test.ts -t "should use the latest options": passes on base and with the change; fails if start() does not reset the session (Received number of calls: 0).
  • yarn ci (clean, lint, test:coverage, build, types:test): passes; 58 suites / 337 tests, coverage thresholds met, attw reports no problems.

CheckList

  • Has been tested (where required).

Calling userEvent.setup() in start() prepares the document and the
currently focused element even when only navigation commands are used.
When the consumer has a separate copy of @testing-library/user-event,
this double-prepares a focused input and breaks later userEvent
interactions on it (regression in 0.33.0).

Fixes guidepup#250
@cmorten

cmorten commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

A walkthrough that calls act(), click(), press() or type() still creates a userEvent session at that point, so with two copies of @testing-library/user-event it can still hit the original problem on an input focused at that moment.

This sits awkwardly and makes me hesitant about doing a partial fix, ideally we resolve completely.

Uncertain atm if there is a way to achieve without starting to expose the implementation detail that we use user-event ideally the interface wouldn’t ever couple to the testing lib package given we could choose to swap out the internals in future… I guess could be an abstraction where method delegates could be passed in, with the defaults being our internal userEvent instance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

virtual.start() in 0.33.0 breaks subsequent userEvent interactions on the element that was focused when start() was called

2 participants