Expand description
§8. Testing
The app is assembled; now make its two important interactions executable: type a
todo and press Enter, then delete one and advance the undo timer. TestApp runs
both paths without a display.
§Let tests set up the app
Tests need the same key bindings as the application. Move them out of main into
a function that both entry points call:
/// The text field's editing keys, and Enter to add a todo.
fn bind_keys(cx: &mut App) {
cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT)));
cx.bind_keys([KeyBinding::new("enter", AddTodo, Some("Todos"))]);
}and call bind_keys(cx) at the start of the closure in main.
GPUI’s testing tools are behind its test-support feature, so enable it for
tests only:
[dev-dependencies]
gpui = { package = "gpui-ce", git = "https://github.com/gpui-ce/gpui-ce", features = ["test-support"] }§Test typing a todo
Add a test module at the bottom of src/main.rs:
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestApp;
#[test]
fn enter_adds_a_todo_and_clears_the_field() {
let mut app = TestApp::new();
app.update(bind_keys);
let mut window = app.open_window(Todos::new);
window.simulate_input("Milk");
window.simulate_keystrokes("enter");
window.read(|todos, cx| {
assert_eq!(todos.todos.len(), 1);
assert_eq!(todos.todos[0].title, "Milk");
assert_eq!(todos.input.read(cx).as_str(), "");
});
}
}Run it with cargo test.
TestApp is a complete GPUI app with a pretend platform: windows exist, render
and lay out, but nothing is drawn to a screen. open_window
builds your view the same way main does. The window it returns can simulate
input, like typing characters and pressing keys, and can read or update the view.
This follows the production path: focus sends characters to the field, Enter
matches the Todos binding, and add mutates the list. A broken focus, binding,
or handler fails at the boundary where it matters.
§Test the undo timeout
Tasks and timers are testable too. TestApp uses a simulated clock that advances
only when requested, so the four-second timeout takes no wall-clock time:
#[test]
fn a_deleted_todo_can_be_restored_until_the_offer_expires() {
let mut app = TestApp::new();
let mut window = app.open_window(Todos::new);
window.update(|todos, _, cx| {
todos.todos.push(Todo { id: 0, title: "Bread".into(), done: false });
todos.delete(0, cx);
todos.undo(cx);
});
window.read(|todos, _| assert_eq!(todos.todos.len(), 1));
window.update(|todos, _, cx| todos.delete(0, cx));
app.advance_clock(Duration::from_secs(5));
window.read(|todos, _| assert!(todos.deleted.is_none()));
}advance_clock moves the simulated time forward and
runs everything that became ready, including the task waiting on the timer.
The test advances the clock past the deadline before reading the view. The
explicit clock makes the assertion independent of wall-clock and scheduler timing.
§Where to go next
You now have a complete GPUI app with headless checks for input and timeout behavior. The finished todos example contains the full source. From here, use the documentation according to what you want to learn next:
- The guides solve specific problems, from long lists to custom drawing.
- The concepts explain how GPUI’s pieces fit together, starting with ownership and data flow.
- The examples show each part of GPUI in a complete program.
- The testing guide covers
#[gpui::test]for async tests, randomized scheduling, and more.