cranpose-ui 0.21.1

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use super::*;

#[test]
fn a_draw_closure_can_name_its_own_node_for_the_next_frame() {
    let _scope = app_context_test_scope();
    let _ = take_draw_repass_nodes();
    let _ = take_render_invalidation();

    observe_draw_reads(DrawObservationScope::new(NodeId(33), 0, 0), || {
        request_current_draw_redraw();
    });
    assert_eq!(
        take_draw_repass_nodes(),
        vec![NodeId(33)],
        "the recording node must be scheduled for re-record"
    );
    assert!(
        take_render_invalidation(),
        "the next frame must be requested"
    );

    request_current_draw_redraw();
    assert!(take_draw_repass_nodes().is_empty());
    assert!(take_render_invalidation());
}

#[test]
fn app_context_ids_do_not_use_process_global_counter() {
    let source = include_str!("../render_state.rs");
    assert!(!source.contains(concat!("NEXT_", "APP_CONTEXT_ID: Atomic")));
}

#[test]
fn app_context_ids_are_unique_within_thread_registry() {
    let first = AppContext::new();
    let second = AppContext::new();

    assert_ne!(first.id, second.id);
    assert!(app_context_by_id(first.id).is_some());
    assert!(app_context_by_id(second.id).is_some());
}

#[test]
fn set_text_fonts_requires_active_app_context() {
    let result = std::panic::catch_unwind(|| {
        crate::text::set_text_fonts(crate::text::test_fonts());
    });
    assert!(result.is_err());

    let context = AppContext::new();
    context.enter(|| {
        crate::text::set_text_fonts(crate::text::test_fonts());
    });
}

#[test]
fn the_font_scale_starts_at_one_and_invalidates_layout_when_it_moves() {
    let context = AppContext::new();
    context.enter(|| {
        assert_eq!(current_font_scale(), 1.0);
        let _ = take_layout_invalidation();

        set_font_scale(1.3);
        assert_eq!(current_font_scale(), 1.3);
        assert!(
            take_layout_invalidation(),
            "every Sp on screen just changed size"
        );

        set_font_scale(1.3);
        assert!(!take_layout_invalidation());
    });
}

#[test]
fn scoped_repasses_neither_request_nor_cancel_a_global_layout_invalidation() {
    let context = AppContext::new();
    context.enter(|| {
        let _ = take_layout_invalidation();

        schedule_layout_repass(NodeId(3));
        schedule_measure_repass(NodeId(4));
        assert!(
            !peek_layout_invalidation(),
            "a scoped repass must not re-measure the whole tree"
        );
        assert!(has_pending_layout_repasses());
        assert!(has_pending_measure_repasses());

        set_font_scale(1.5);
        schedule_layout_repass(NodeId(5));
        assert!(
            take_layout_invalidation(),
            "a scoped repass must not swallow the font scale change"
        );
    });
}

#[test]
fn a_font_scale_no_platform_reports_is_refused() {
    let context = AppContext::new();
    context.enter(|| {
        for nonsense in [0.0, -1.0, f32::NAN, f32::INFINITY] {
            set_font_scale(1.0);
            set_font_scale(nonsense);
            assert_eq!(current_font_scale(), 1.0, "{nonsense} was let through");
        }
        set_font_scale(99.0);
        assert_eq!(current_font_scale(), MAX_FONT_SCALE);
        set_font_scale(0.01);
        assert_eq!(current_font_scale(), MIN_FONT_SCALE);
    });
}

#[test]
fn the_font_scale_is_per_app_context() {
    let first = AppContext::new();
    let second = AppContext::new();
    first.enter(|| set_font_scale(1.5));
    first.enter(|| assert_eq!(current_font_scale(), 1.5));
    second.enter(|| assert_eq!(current_font_scale(), 1.0));
}

#[test]
fn app_contexts_keep_density_and_invalidations_isolated() {
    let first = AppContext::new_with_density(1.0, None);
    let second = AppContext::new_with_density(1.0, None);

    first.enter(|| {
        set_density(2.0);
        request_render_invalidation();
        request_pointer_invalidation();
        schedule_layout_repass(NodeId(11));
        schedule_draw_repass(NodeId(12));
    });

    second.enter(|| {
        assert_eq!(current_density(), 1.0);
        assert!(!peek_render_invalidation());
        assert!(!peek_pointer_invalidation());
        assert!(!peek_layout_invalidation());
        assert!(!has_pending_layout_repasses());
        assert!(!has_pending_draw_repasses());
    });

    first.enter(|| {
        assert_eq!(current_density(), 2.0);
        assert!(peek_render_invalidation());
        assert!(peek_pointer_invalidation());
        assert!(peek_layout_invalidation());
        assert!(has_pending_layout_repasses());
        assert!(has_pending_draw_repasses());
        assert_eq!(take_layout_repass_nodes(), vec![NodeId(11)]);
        assert_eq!(take_draw_repass_nodes(), vec![NodeId(12)]);
        assert!(take_render_invalidation());
        assert!(take_pointer_invalidation());
        assert!(take_layout_invalidation());
    });
}

#[test]
fn app_contexts_keep_fling_velocity_diagnostics_isolated() {
    let first = AppContext::new_with_density(1.0, None);
    let second = AppContext::new_with_density(1.0, None);

    first.enter(|| {
        record_last_fling_velocity(1200.0);
        assert_eq!(debug_last_fling_velocity(), 1200.0);
    });

    second.enter(|| {
        assert_eq!(debug_last_fling_velocity(), 0.0);
        record_last_fling_velocity(-450.0);
        assert_eq!(debug_last_fling_velocity(), -450.0);
    });

    first.enter(|| {
        assert_eq!(debug_last_fling_velocity(), 1200.0);
        debug_reset_last_fling_velocity();
        assert_eq!(debug_last_fling_velocity(), 0.0);
    });

    second.enter(|| {
        assert_eq!(debug_last_fling_velocity(), -450.0);
    });
}

#[test]
fn app_context_new_uses_independent_density() {
    let outer = AppContext::new_with_density(2.0, None);
    let context = AppContext::new();
    context.enter(|| {
        assert_eq!(current_density(), 1.0);
    });
    outer.enter(|| {
        assert_eq!(current_density(), 2.0);
    });
}

#[test]
fn runtime_state_access_requires_explicit_app_context_even_in_tests() {
    let result = std::panic::catch_unwind(|| {
        request_render_invalidation();
    });
    assert!(result.is_err());
}

#[test]
fn app_contexts_keep_layout_frame_arenas_isolated() {
    let first = AppContext::new_with_density(1.0, None);
    let second = AppContext::new_with_density(1.0, None);

    first.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
        let mut arena = first.take_layout_frame_arena();
        arena.seed_placement_scratch_for_test();
        first.replace_layout_frame_arena(arena);
        assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
    });

    second.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 0);
    });

    first.enter(|| {
        assert_eq!(layout_frame_arena_placement_scratch_count(), 1);
    });
}

#[test]
fn current_app_context_scope_does_not_extend_context_lifetime() {
    let weak = {
        let context = AppContext::new_with_density(1.0, None);
        let weak = Rc::downgrade(&context);
        context.enter(|| {
            assert!(current_app_context().is_some());
        });
        weak
    };

    assert!(weak.upgrade().is_none());
    assert!(current_app_context().is_none());
}

#[test]
fn dropped_app_context_unregisters_from_thread_lookup_registry() {
    let start_count = app_context_registry_entry_count();

    let id = {
        let context = AppContext::new_with_density(1.0, None);
        let id = context.id;
        assert!(app_context_by_id(id).is_some());
        id
    };

    assert_eq!(
        app_context_registry_entry_count(),
        start_count,
        "dropped AppContexts must remove their weak registry entry"
    );
    assert!(app_context_by_id(id).is_none());
}