Skip to main content

Module ch8_testing

Module ch8_testing 

Source
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: