Skip to content

Introduce a crate to write unit tests of Dioxus apps#5323

Open
hovinen wants to merge 9 commits intoDioxusLabs:mainfrom
hovinen:introduce-testing-library
Open

Introduce a crate to write unit tests of Dioxus apps#5323
hovinen wants to merge 9 commits intoDioxusLabs:mainfrom
hovinen:introduce-testing-library

Conversation

@hovinen
Copy link
Copy Markdown

@hovinen hovinen commented Feb 20, 2026

This is a preliminary version of a library in the spirit of React or Flutter to allow easy unit testing of Dioxus apps.

Right now it only supports the click event, but I believe that is sufficient to show the design. Once the overall design is finalised, it can be fully fleshed out.

I placed this directly in the Dioxus repository. I'm also fine with keeping it as a separate crate if desired. However, this requires a few small changes to the dioxus-native crate to allow the creation of synthetic events. Otherwise the existing visibility restrictions make it impossible to dispatch DOM events from the test.

I decided to add fairly fleshed out documentation including doctests so as to illustrate the use of the library. I have tested it against a personal project and it is working quite well.

Alternatives considered

  • Writing a new renderer. I had considered this but found that it would be pretty much a duplicate of the existing dioxus-native.
  • Dispatching events via Document::handle_ui_event. This would mean that the events would act more like real user-generated events, where, say, a click happens at particular coordinates and the framework identifies the element which is hit. This would be more "realistic" in the sense that a click on a button which is covered by another element would not hit the button but rather the element covering it. However, the UiEvent enum from Blitz does not support a "click" event and I did not want to create multiple PRs in different repositories for now.

Fixes #5324

To be turned into prose:

- Existing testing options are dioxus-ssr and Playwright
  - dioxus-ssr does not allow interaction with the DOM in a test. As soon
    as the test needs to click a button, it won't work.
  - Playwright is extremely heavy-weight. Most importantly, it creates a
    technological divide between the test code and the component under
    test. This makes it really hard to instrument the component under
    test from the test itself. Writing new tests is a major undertaking.
- I want it to be easy to write new tests. And the tests should execute
  very quickly in the normal case.

- Inspired by testing libraries in Flutter and React
- Based on dioxus-native and blitz
- Test creates a `Tester` by rendering into it. It can then query
  elements to obtain `TestElement` instances which reference the DOM.
  They can dispatch events via methods on those functions.
- For async and handling events: call `Tester::pump`. This awaits so that
  it passes control back to the runtime to handle any other async tasks.
- For interaction: events dispatched through the VirtualDom
- This requires a change to dioxus-native: we need to be able to
  construct synthetic events. Previously, visibility restrictions
  prevented that. This adds a function to generate a synthetic click
  analogous to the function in Blitz.

- In this PR only "click" is supported. Further events can be added but I
  would like feedback before fleshing it out.
- Since events go through the VirtualDom, they hit the target element
  directly. If the target element is covered by a frost or is otherwise
  inaccessible, then the click will have no effect in reality but will
  still work in the test.
- The same might be true of disabled elements.

- Writing a new renderer
  - Seems redundant since the renderer in dioxus-native serves the needs
    of this library perfectly.
- Dispatching events via `DioxusDocument::handle_ui_event`:
  - Layout with Blitz does not work reliably enough
  - There is no "click" event variant in the `UiEvent` enum. So this
    would require a change in Blitz as well.
@hovinen hovinen requested a review from a team as a code owner February 20, 2026 08:29
timeout(PUMP_TIMEOUT, self.document.vdom.wait_for_work()).await?;
while self.document.poll(None) {}
Ok(())
}
Copy link
Copy Markdown
Member

@ealmloff ealmloff Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very excited about this, thanks for working on it!

Instead of requiring explicit event pumping, we could mimic the Playwright test API and make queries asynchronously run the event loop:

let mut tester = dioxus_test::render(AComponent).build();
// Wait for make-request-button to load and click it to make a network request.
tester.find_by_test_id("make-request-button").click().await?;
// Assert on the state of the UI while the network request is in flight.
expect(tester.find_by_test_id("loading-indicator")).to_be_visible().await?;
// Receive the server response and assert on the state of the UI after the response
// is received and the UI has been rerendered.
expect(tester.find_by_test_id("resolved-content")).to_be_visible().await?;

In some cases, waiting for work and applying a single tick of DOM operations could provide more control over testing, but I think the familiarity of the Playwright-style API and intuitive behavior in the presence of feedback loops with DOM events are typically more important.

For example, onmounted and use_effect will only trigger after the document has applied edits. With the current model, I think that would require rendering, then waiting for a 0ms timeout, then asserting the state has changed based on the onmounted event. If asserting on that state also pumped the DOM, you could just assert the state is correct directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks -- let me try this and see what I can come up with.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Evan, thanks for your patience on this. It took me a while to find a workable structure for this, and I admittedly didn't have as much to work on it as I would have liked.

I settled on a second layer on top of the initial API. So one can use the pump method to drive the event loop, but there's a higher level API which lets the caller await conditions on the DOM and assert on those.

Since the tester doesn't know when all async operations have fully settled, it has to rely on heuristics to some degree. It tries up to a maximum number of attempts to obtain an element or to make an assertion and gives up eventually. The pump method is similarly equipped with a timeout since I don't have an easy way to detect whether it would just wait forever.

I had to introduce some new concepts to support assertions in this model. A given element on which the test is to assert might already be present but have the wrong state at the time the assertion is invoked. So there needs to be a concept of "assert that this is eventually true, even if it's not true right now." To support this, I introduced matchers similar to the GoogleTest crate. These can be passed into the tester which can just repeatedly try the assertion until it's true, giving up after a maximum number of tries.

I've tried this out on a private project and the API seems reasonable.

There are some more TODOs on this: I need to update the overall docs and I'd like to make some improvements to the API. But this should at least convey the idea.

Please let me know what you think. Thanks!

Copy link
Copy Markdown
Member

@ealmloff ealmloff Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much nicer! We could make this more composable by extracting a poll once or await helper so you can do something like:

tester.get("selector").immediately()?;
tester.get("selector").await?;
tester.get("selector").expect(inner_html(to_contain("hello"))).await?;
tester.get("selector").expect(inner_html(to_contain("hello"))).immediately()?;

It isn't fully built out yet, but I have a prototype in this commit: 358b9ed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I had played around trying to build a fluent interface like that but found that whatever the query method on DocumentTester returned had to contain an exclusive reference to DocumentTester, which caused no end of problems. I admittedly didn't consider implementing IntoFuture directly.

I'll play around with your prototype and see how well it works in the project I am doing.

@ealmloff ealmloff added enhancement New feature or request native Related to dioxus-native labels Mar 3, 2026
@hovinen hovinen requested a review from ealmloff March 23, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request native Related to dioxus-native

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dioxus testing library

2 participants