cranpose-testing 0.1.14

Testing utilities and harness for Cranpose
Documentation
//! Integration tests for pointer input with async handlers and button interactions

use cranpose_core::MutableState;
use cranpose_foundation::PointerEventKind;
use cranpose_macros::composable;
use cranpose_testing::ComposeTestRule;
use cranpose_ui::*;

#[composable]
fn hover_tracking_app(hover_position: MutableState<Point>, is_hovered: MutableState<bool>) {
    Column(
        Modifier::empty()
            .padding(20.0)
            .then(Modifier::empty().size(Size {
                width: 200.0,
                height: 200.0,
            }))
            .then(Modifier::empty().pointer_input((), {
                let position = hover_position;
                let hovered = is_hovered;
                move |scope: PointerInputScope| async move {
                    scope
                        .await_pointer_event_scope(|await_scope| async move {
                            loop {
                                let event = await_scope.await_pointer_event().await;
                                match event.kind {
                                    PointerEventKind::Move => {
                                        position.set(Point {
                                            x: event.position.x,
                                            y: event.position.y,
                                        });
                                        hovered.set(true);
                                    }
                                    PointerEventKind::Cancel => {
                                        hovered.set(false);
                                    }
                                    _ => {}
                                }
                            }
                        })
                        .await;
                }
            })),
        ColumnSpec::default(),
        || {
            Text(
                "Hover area",
                Modifier::empty().padding(8.0),
                TextStyle::default(),
            );
        },
    );
}

#[test]
fn test_pointer_input_async_handler_is_present() {
    let _app_context = cranpose_ui::AppContext::new();
    let _app_context_scope = _app_context.enter_scope();
    _app_context.enter(cranpose_ui::reset_render_state_for_tests);
    // This test verifies that async pointer_input handlers are properly
    // extracted into the modifier chain and available for hit-testing

    let mut rule = ComposeTestRule::new();
    let runtime = rule.runtime_handle();

    let hover_position = MutableState::with_runtime(Point { x: 0.0, y: 0.0 }, runtime.clone());
    let is_hovered = MutableState::with_runtime(false, runtime.clone());

    rule.set_content({
        let pos = hover_position;
        let hovered = is_hovered;
        move || {
            hover_tracking_app(pos, hovered);
        }
    })
    .expect("initial render succeeds");

    // Verify initial state
    assert_eq!(hover_position.get().x, 0.0);
    assert_eq!(hover_position.get().y, 0.0);
    assert!(!is_hovered.get());

    // The composition should have created a Column with a pointer_input modifier
    let node_count = rule.applier_mut().len();
    assert!(
        node_count >= 2,
        "Should have at least 2 nodes (Column and Text)"
    );

    // This test validates composition structure. The app-shell test suite covers
    // hit-testing and pointer dispatch with rendered scenes.

    println!(
        "✓ Pointer input composition created successfully with {} nodes",
        node_count
    );
}

#[composable]
fn button_with_modifiers_app(click_count: MutableState<i32>) {
    Column(
        Modifier::empty().padding(20.0),
        ColumnSpec::default(),
        move || {
            Text(
                format!("Clicks: {}", click_count.get()),
                Modifier::empty().padding(8.0),
                TextStyle::default(),
            );

            // Button with draw_behind modifier (like the pause button)
            Button(
                Modifier::empty()
                    .rounded_corners(12.0)
                    .then(Modifier::empty().draw_behind(|scope| {
                        scope.draw_round_rect(
                            Brush::solid(Color(0.2, 0.45, 0.9, 1.0)),
                            CornerRadii::uniform(12.0),
                        );
                    })),
                ButtonSpec::default(),
                {
                    let count = click_count;
                    move || {
                        count.set(count.get() + 1);
                    }
                },
                || {
                    Text(
                        "Click me",
                        Modifier::empty().padding(4.0),
                        TextStyle::default(),
                    );
                },
            );
        },
    );
}

#[test]
fn test_button_with_draw_modifiers_is_clickable() {
    let _app_context = cranpose_ui::AppContext::new();
    let _app_context_scope = _app_context.enter_scope();
    _app_context.enter(cranpose_ui::reset_render_state_for_tests);
    // This test verifies that buttons with draw_behind modifiers are still clickable
    // This reproduces the "pause button" issue where buttons with custom rendering
    // might not have their click handlers properly wired

    let mut rule = ComposeTestRule::new();
    let runtime = rule.runtime_handle();

    let click_count = MutableState::with_runtime(0, runtime.clone());

    rule.set_content({
        let count = click_count;
        move || {
            button_with_modifiers_app(count);
        }
    })
    .expect("initial render succeeds");

    // Verify initial state
    assert_eq!(click_count.get(), 0);

    // The button should have been created with all modifiers
    // including both the user's draw_behind and the internal clickable
    let node_count = rule.applier_mut().len();
    assert!(
        node_count >= 3,
        "Should have at least 3 nodes (Column, Text, Button)"
    );

    println!(
        "✓ Button with draw modifiers created successfully with {} nodes",
        node_count
    );
}

#[composable]
fn dynamic_label_button_app(click_count: MutableState<i32>, is_active: MutableState<bool>) {
    Column(
        Modifier::empty().padding(20.0),
        ColumnSpec::default(),
        move || {
            let active = is_active.get();
            let label = if active { "Active" } else { "Inactive" };

            Button(
                Modifier::empty().padding(10.0),
                ButtonSpec::default(),
                {
                    let count = click_count;
                    move || {
                        is_active.set(!is_active.get());
                        count.set(count.get() + 1);
                    }
                },
                {
                    let label_str = label.to_string();
                    move || {
                        Text(
                            label_str.clone(),
                            Modifier::empty().padding(4.0),
                            TextStyle::default(),
                        );
                    }
                },
            );
        },
    );
}

#[test]
fn test_button_with_dynamic_content_updates_correctly() {
    let _app_context = cranpose_ui::AppContext::new();
    let _app_context_scope = _app_context.enter_scope();
    _app_context.enter(cranpose_ui::reset_render_state_for_tests);
    // This test ensures buttons with dynamic labels (like pause/resume)
    // properly update and remain clickable after state changes

    let mut rule = ComposeTestRule::new();
    let runtime = rule.runtime_handle();

    let click_count = MutableState::with_runtime(0, runtime.clone());
    let is_active = MutableState::with_runtime(false, runtime.clone());

    rule.set_content({
        let count = click_count;
        let active = is_active;
        move || {
            dynamic_label_button_app(count, active);
        }
    })
    .expect("initial render succeeds");

    // Verify initial state
    assert_eq!(click_count.get(), 0);
    assert!(!is_active.get());

    // Manually toggle the state (simulating a click)
    is_active.set(true);
    click_count.set(1);

    // Force recomposition
    rule.pump_until_idle()
        .expect("recompose after state change");

    // Verify state updated
    assert_eq!(click_count.get(), 1);
    assert!(is_active.get());

    // Toggle again
    is_active.set(false);
    click_count.set(2);

    rule.pump_until_idle()
        .expect("recompose after second toggle");

    assert_eq!(click_count.get(), 2);
    assert!(!is_active.get());

    println!("✓ Button with dynamic content updates correctly through state changes");
}