Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
23a9be0
Introduce a testing library for Dioxus
hovinen Feb 17, 2026
cb1c416
Flesh out crate docs
hovinen Feb 19, 2026
5f66041
Fix doctests
hovinen Feb 19, 2026
0a82eff
Fix typos
hovinen Feb 21, 2026
75f52bb
Fix clippy warnings
hovinen Feb 21, 2026
68f601f
Fix doc warnings
hovinen Feb 21, 2026
1166bf2
Introduce a higher level interface including waiting for conditions
hovinen Mar 21, 2026
76c629b
Add some documentation
hovinen Mar 22, 2026
61a6705
Add not matcher
hovinen Mar 22, 2026
e7767e5
a bit more composable
ealmloff Mar 23, 2026
8b7683e
WIP: Support assertions in fluent interface
hovinen Apr 3, 2026
2b950f6
WIP: Go back to type erasure
hovinen Apr 3, 2026
1a204da
Fix remaining problems
hovinen Apr 3, 2026
dcbb8d3
Make Waitable private again
hovinen Apr 3, 2026
5f797b8
WIP: Support matching on collections
hovinen Apr 4, 2026
1ab204e
Fix remaining problems matching on collections
hovinen Apr 11, 2026
ed386e9
Move conditions to a separate module
hovinen Apr 12, 2026
b4d9798
Eliminate ImmediateCondition as a trait, fold methods into normal imp…
hovinen Apr 13, 2026
a97fe28
Update docs
hovinen Apr 13, 2026
54bc071
Update more docs
hovinen Apr 13, 2026
896806f
Fix doctests
hovinen Apr 14, 2026
11b4d9d
Improve documentation further
hovinen Apr 14, 2026
b7b5d5f
Fix doctest
hovinen Apr 14, 2026
03ed7a8
Clippy
hovinen Apr 14, 2026
0d64fe2
Add more docs
hovinen Apr 15, 2026
09220bf
More examples
hovinen Apr 16, 2026
af1c629
Fix doctests
hovinen Apr 16, 2026
9d29175
Make not() return the concrete type NotMatcher so that assertions usi…
hovinen Apr 17, 2026
0b9f7eb
More docs
hovinen Apr 17, 2026
f0cd255
Make assertion failure descriptions somewhat reasonable and make AllE…
hovinen Apr 18, 2026
d38954d
Resolve TODOs
hovinen Apr 18, 2026
2da3523
Remove Waitable impl for AllElementsCondition
hovinen Apr 18, 2026
4c8c982
Flesh out more docs
hovinen Apr 18, 2026
c737a9f
Doc fixes
hovinen Apr 18, 2026
5527093
Add warning to docs
hovinen Apr 18, 2026
f698c43
Expand warning with correct implementation
hovinen Apr 18, 2026
cb887ca
Clarify root()
hovinen Apr 18, 2026
1bc4838
Merge branch 'main' into introduce-testing-library
hovinen Apr 18, 2026
3d721f0
Reduce duplication
hovinen Apr 18, 2026
741f601
Cargo.lock
hovinen Apr 18, 2026
f78f003
Reduce lines
hovinen Apr 19, 2026
66f8c74
Move feature spec to test package
hovinen Apr 19, 2026
dd66555
Review comment
hovinen Apr 19, 2026
bc72605
Fix typos
hovinen Apr 20, 2026
4a7627d
Fix doc links
hovinen Apr 20, 2026
a2a152a
Fix doctests
hovinen Apr 20, 2026
e414385
Merge branch 'main' into introduce-testing-library
hovinen Apr 24, 2026
793903b
Update Cargo.lock
hovinen Apr 24, 2026
dc4cd4f
Merge branch 'main' into introduce-testing-library
hovinen May 5, 2026
58e9ab3
Reintroduce taffy dependency
hovinen May 5, 2026
7ab8351
Update with 0.7.6 API changes
hovinen May 6, 2026
58a1946
Import taffy exactly the way blitz-dom does
hovinen May 6, 2026
4f87b72
Remove taffy dependency
hovinen May 6, 2026
ce71740
Use workspace tokio dependency
hovinen May 6, 2026
30d1644
Merge branch 'main' into introduce-testing-library
hovinen May 23, 2026
8debf08
Remove spuriously added line
hovinen May 25, 2026
fe65b54
Cargo.lock
hovinen May 25, 2026
d56dccb
Use sequential StyleThreading as per https://github.com/DioxusLabs/bl…
hovinen Jun 1, 2026
7b02593
Methods in ElementCondition and AllElementsCondition now take &self
hovinen Jun 1, 2026
8691398
Merge branch 'main' into introduce-testing-library
hovinen Jun 8, 2026
49d58f3
Use matchers from test-that rather than own matchers
hovinen Jun 2, 2026
c110ffa
Update tests
hovinen Jun 8, 2026
5e596a2
Adjust dep version
hovinen Jun 9, 2026
d37fc27
Upgrade test-that dependency
hovinen Jun 28, 2026
f7d1933
Put matchers in matchers module and reexport them
hovinen Jun 28, 2026
270b4ee
Merge branch 'main' into introduce-testing-library
hovinen Jun 28, 2026
e9ece73
Have query take a shared reference rather than an exclusive reference
hovinen Jun 28, 2026
63dbfd0
Add test of multiple parallel queries, and make testers in the doctes…
hovinen Jun 28, 2026
745e0a2
Move query up in test
hovinen Jun 28, 2026
95cc52f
Remove unneeded lifetime parameter
hovinen Jun 29, 2026
d84801e
Bump test-that dependency to 0.5.1
hovinen Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ members = [
"packages/asset-resolver",
"packages/depinfo",
"packages/component-manifest",
"packages/test",

# CLI harnesses, all included
"packages/cli-harnesses/*",
Expand Down Expand Up @@ -209,6 +210,7 @@ wgpu_context = { version = "0.1", default-features = false }
wgpu = { version = "26.0" }
vello = "0.6"
bevy = "0.17"
taffy = "0.9.2"


# a fork of pretty please for tests - let's get off of this if we can!
Expand All @@ -220,7 +222,7 @@ tracing = "0.1.41"
tracing-futures = "0.2.5"
tracing-subscriber = { version = "0.3.19", default-features = false }
toml = "0.8"
tokio = "1.48"
tokio = { version = "1.48", features = ["time"] }
Comment thread
hovinen marked this conversation as resolved.
Outdated
tokio-util = { version = "0.7.15" }
tokio-stream = { version = "0.1.17" }
slab = "0.4.10"
Expand Down
5 changes: 5 additions & 0 deletions packages/native-dom/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use blitz_dom::Node;
use blitz_traits::events::{BlitzKeyEvent, BlitzMouseButtonEvent, MouseEventButton};
use dioxus_html::{
geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint},
Expand Down Expand Up @@ -229,3 +230,7 @@ impl HasFocusData for NativeFocusData {
self as &dyn std::any::Any
}
}

pub fn synthetic_click_event(node: &Node, modifiers: Modifiers) -> Box<dyn Any> {
Box::new(NativeClickData(node.synthetic_click_event_data(modifiers)))
}
Comment thread
hovinen marked this conversation as resolved.
1 change: 1 addition & 0 deletions packages/native-dom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod events;
mod mutation_writer;
pub use blitz_dom::DocumentConfig;
pub use dioxus_document::DioxusDocument;
pub use events::synthetic_click_event;

use blitz_dom::{ns, LocalName, Namespace, QualName};
type NodeId = usize;
Expand Down
23 changes: 23 additions & 0 deletions packages/test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "dioxus-test"
version = { workspace = true }
authors = ["Bradford Hovinen"]
edition = "2024"
description = "Test library for Dioxus based on native renderer"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/learn/0.7/getting_started"
keywords = ["dom", "ui", "gui", "react", "testing"]

[dependencies]
blitz-dom = { workspace = true }
dioxus-core = { workspace = true }
dioxus-html = { workspace = true }
dioxus-native-dom = { workspace = true }
taffy = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
dioxus = { workspace = true }
tokio = { version = "1.48", features = ["rt", "macros"] }
177 changes: 177 additions & 0 deletions packages/test/src/element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use dioxus_core::{ElementId, Event};
use dioxus_html::{
Modifiers, PlatformEventData,
geometry::{Coordinates, euclid::Point2D},
};
use dioxus_native_dom::{DioxusDocument, synthetic_click_event};
use std::rc::Rc;

/// A reference to DOM node managed by a [crate::Tester].
///
/// This provides facilities for interacting with the node, querying its layout properties, and
/// obtaining its content.
pub struct TestElement<'doc> {
pub(crate) document: &'doc DioxusDocument,
pub(crate) node: &'doc blitz_dom::Node,
}

impl<'vdom> TestElement<'vdom> {
/// Synonym of [TestElement::click].
pub fn tap(&self) {
self.click()
}

/// Dispatches a `click` event on this element.
///
/// The exact location of the click is unspecified.
///
/// If the element has an `onclick` handler, it will be invoked once [crate::Tester::pump] is
/// called.
pub fn click(&self) {
self.send_event(
"click",
Event::new(
Rc::new(PlatformEventData::new(synthetic_click_event(
self.node,
Modifiers::empty(),
))),
true,
),
)
}

/// Sends an event with the given `name` to this element.
///
/// The event is registered with the Dioxus runtime. A subsequent call to [crate::Tester::pump]
/// causes the event handler to be invoked, if one is present.
///
/// If no event handler is registered corresponding to the event `name`, then this method has no
/// effect.
///
/// This operates directly on the element, so that is is guaranteed to receive the event. This
/// might not reflect how the element would respond in reality. For example, a click at the
/// coordinates of a button which is behind a frost element will not reach the button. But this
/// method behaves as though it would.
///
/// The `event` parameter must contain a [PlatformEventData] with a payload corresponding to the
/// specific event type. This method panics if the event payload has the wrong type.
pub fn send_event(&self, name: &str, event: Event<PlatformEventData>) {
let propagates = event.propagates();
self.document.vdom.runtime().handle_event(
name,
Event::new(event.data, propagates),
self.get_element_id()
.expect("Expected element to have a Dioxus ID"),
);
}

/// Returns a `String` consisting of the HTML of this element and all of its children.
pub fn outer_html(&self) -> String {
self.node.outer_html()
}

/// Returns a `String` consisting of the HTML of this element's children, not including this
/// element itself.
pub fn inner_html(&self) -> String {
let inner_html_parts: Vec<_> = self
.node
.children
.iter()
.filter_map(|child_id| {
self.document
.get_node(*child_id)
.map(|child| child.outer_html())
})
.collect();
inner_html_parts.join("")
}

/// Returns the calculated [Coordinates] of the centre of this element.
pub fn center(&self) -> Coordinates {
let upper_left = self.upper_left();
let lower_right = self.lower_right();
Coordinates::new(
upper_left.screen().lerp(lower_right.screen(), 0.5),
upper_left.client().lerp(lower_right.client(), 0.5),
upper_left.element().lerp(lower_right.element(), 0.5),
upper_left.page().lerp(lower_right.page(), 0.5),
)
}

/// Returns the calculated [Coordinates] of the upper-left corner of this element.
pub fn upper_left(&self) -> Coordinates {
let upper_left = self.node.final_layout.location;
Coordinates::new(
Self::to_point2d(upper_left),
Self::to_point2d(upper_left),
Self::to_point2d(upper_left),
Self::to_point2d(upper_left),
)
}

/// Returns the calculated [Coordinates] of the upper-right corner of this element.
pub fn upper_right(&self) -> Coordinates {
let mut upper_right = self.node.final_layout.location;
upper_right.x += self.node.final_layout.content_box_width();
Coordinates::new(
Self::to_point2d(upper_right),
Self::to_point2d(upper_right),
Self::to_point2d(upper_right),
Self::to_point2d(upper_right),
)
}

/// Returns the calculated [Coordinates] of the lower-left corner of this element.
pub fn lower_left(&self) -> Coordinates {
let mut lower_left = self.node.final_layout.location;
lower_left.y += self.node.final_layout.content_box_height();
Coordinates::new(
Self::to_point2d(lower_left),
Self::to_point2d(lower_left),
Self::to_point2d(lower_left),
Self::to_point2d(lower_left),
)
}

/// Returns the calculated [Coordinates] of the lower-right corner of this element.
pub fn lower_right(&self) -> Coordinates {
let mut lower_right = self.node.final_layout.location;
lower_right.x += self.node.final_layout.content_box_width();
lower_right.y += self.node.final_layout.content_box_height();
Coordinates::new(
Self::to_point2d(lower_right),
Self::to_point2d(lower_right),
Self::to_point2d(lower_right),
Self::to_point2d(lower_right),
)
}

fn to_point2d<Space>(point: taffy::geometry::Point<f32>) -> Point2D<f64, Space> {
Point2D::new(point.x as f64, point.y as f64)
}

/// Returns the calculated size of this element as a tuple (width, height) in screen pixels.
pub fn size(&self) -> (f32, f32) {
let height = self.node.final_layout.content_box_height();
let width = self.node.final_layout.content_box_width();
(width, height)
}

fn get_element_id(&self) -> Option<ElementId> {
self.node
.element_data()?
.attrs
.iter()
.find(|attr| *attr.name.local == *"data-dioxus-id")
.and_then(|attr| attr.value.parse::<usize>().ok())
.map(ElementId)
}
}

impl<'doc> std::fmt::Debug for TestElement<'doc> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestElement")
.field("node", &self.node)
.finish()
}
}
103 changes: 103 additions & 0 deletions packages/test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::test_attr_in_doctest)] // The doctests need to show examples of tests
//! A testing crate for Dioxus.
//!
//! This crate facilitates rendering, interacting with, and querying the DOM in tests of Dioxus
//! apps. Tests have fairlz precise control over both the rendering lifecycle and asynchronous
//! operations. Thus they can assert both on the final outcome of interactions, such as the
//! rendered data obtained from a call to a backend, as well as intermediate states, such as the
//! presence of a spinner while loading data.
//!
//! This uses the dioxus-native crate to manage the DOM, which in turn uses
//! [Blitz](https://crates.io/crates/blitz) for layout. It does not depend on a browser or any
//! other external process.
//!
//! Tests operate "headless", so they cannot render their state to the screen.
//!
//! ## Usage
//!
//! Tests can construct a [Tester] instance to render and interact with the DOM. To construct a
//! [Tester], the test can invoke the [render] function on a Dioxus component. They must invoke the
//! `build` to trigger the initial layout. The tester provides methods for querying elements by
//! CSS selector or by test ID.
//!
//! ```
//! use dioxus::prelude::*;
//! use dioxus_test::render;
//!
//! #[component]
//! fn MyComponent() -> Element {
//! rsx! {
//! div {
//! class: "test-component",
//! "Hello, world!"
//! }
//! }
//! }
//!
//! #[test]
//! fn my_component_renders_correctly() {
//! let tester = render(MyComponent).build();
//! assert_eq!(
//! tester.find_by_css_selector(".test-component").unwrap().inner_html(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should find_by_css_selector just panic if it fails? .unwrap() here seems like noise.
(we could also always have a non-panicking try_find_by_css_selector in case someone does need that).

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.

I'm following the philosophy in GoogleTest: don't panic, but rather return a Result and let the caller decide how to handle that.

I'm fine with panicking if that's what you prefer, though.

//! "Hello, world!"
//! );
//! }
//! ```
//!
//! [Tester] also provides methods for interacting with elements and driving the runtime. After
//! interacting with an element, the test must call [Tester::pump] to cause the event handler to be
//! invoked.
//!
//! ```
//! use dioxus::prelude::*;
//! use dioxus_test::render;
//!
//! #[component]
//! fn MyComponent() -> Element {
//! let mut text = use_signal(|| "Click me!");
//! rsx! {
//! button {
//! class: "test-button",
//! onclick: move |_| {
//! *text.write() = "Don't click any more!";
//! },
//! {text}
//! }
//! }
//! }
//!
//! #[tokio::test]
//! async fn my_component_changes_button_text_on_click() {
//! let mut tester = render(MyComponent).build();
//! tester.find_first_by_css_selector(".test-button").unwrap().click();
//! tester.pump().await;
//! assert_eq!(
//! tester.find_first_by_css_selector(".test-button").unwrap().inner_html(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it make sense to have find_* method implicitly pump? (do they not already?). If, so we could remove the explicit pump above.

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.

These docs are outdated -- there is now just such an API. I just haven't updated the docs yet.

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.

I've updated the docs now to reflect the current API. Please take a look.

//! "Don't click any more!"
//! );
//! }
//! ```
//!
//! ## Asynchronous operations
//!
//! The method [Tester::pump] returns control to the async runtime and thus drives any asynchronous
//! operations such as requests to the backend. If any rendering depends on the result of a request,
//! then the test must invoke [Tester::pump] to resolve that.
//!
//! The test can also assert on the state of the DOM while backend requests are in flight.
//!
//! ## Limitations
//!
//! Interactions with the DOM operate directly on elements, not on the screen. So if, say, the test
//! dispatches a click on an element which is covered by a frost, the element will respond as though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's a frost?

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.

I'm referring to a div which covers the page and prevents interaction with elements covered by it, similar to https://css-tricks.com/frosting-glass-css-filters/.

//! it were reachable even though it would not be in reality.
//!
//! The layout system is limited by what the Blitz layout system can support. Since Blitz is not
//! complete as of the time of writing, computed layouts will often not be as in reality.
Comment thread
hovinen marked this conversation as resolved.
Outdated

mod element;
mod tester;

pub use element::TestElement;
pub use tester::{Tester, TesterError, render};
Loading
Loading