use ai::api_keys::CustomEndpointModel; use pathfinder_geometry::vector::vec2f; use warpui::platform::WindowStyle; use warpui::scene::Scene; use warpui::units::Pixels; use warpui::{App, EntityIdSet, Presenter, WindowInvalidation}; use super::*; use crate::test_util::terminal::initialize_app_for_terminal_view; fn endpoint_with_models(model_count: usize) -> CustomEndpoint { CustomEndpoint { name: "Test endpoint".to_string(), url: "https://api.example.com/v1".to_string(), api_key: "key".to_string(), models: (0..model_count) .map(|index| CustomEndpointModel { name: format!("model-{index}"), alias: None, config_key: format!("config-{index}"), }) .collect(), } } fn init_modal_test_models(app: &mut App) { initialize_app_for_terminal_view(app); } fn custom_endpoint_modal_height(scene: &Scene) -> f32 { let rects = scene .layers() .flat_map(|layer| &layer.rects) .map(|rect| (rect.bounds.width(), rect.bounds.height(), rect.border.width)) .collect::>(); rects .iter() .filter(|(width, _, _)| *width > INPUT_WIDTH && *width <= 560.) .map(|(_, height, _)| *height) .max_by(f32::total_cmp) .unwrap_or_else(|| panic!("custom endpoint modal rect should exist: {rects:?}")) } #[test] fn modal_resizes_with_window_and_added_models() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(1); let (window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { let body = ctx.add_typed_action_view(|ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); Modal::new(Some("Edit custom endpoint".to_string()), body, ctx) .with_modal_style(UiComponentStyles { width: Some(560.), ..Default::default() }) .with_max_height_percentage(0.8) }); let body = modal.read(&app, |modal, _| modal.body().clone()); let mut presenter = Presenter::new(window_id); let invalidation = WindowInvalidation { updated: EntityIdSet::from_iter([ app.root_view_id(window_id).expect("root view should exist"), body.id(), ]), ..Default::default() }; app.update(move |ctx| { presenter.invalidate(invalidation.clone(), ctx); let initial_modal_height = { let scene = presenter.build_scene(vec2f(800., 1000.), 1., None, ctx); custom_endpoint_modal_height(&scene) }; body.update(ctx, |body, ctx| { for _ in 0..20 { body.add_model(ctx); } }); presenter.invalidate(invalidation, ctx); let expanded_modal_height = { let scene = presenter.build_scene(vec2f(800., 1000.), 1., None, ctx); custom_endpoint_modal_height(&scene) }; let small_window_height = { let scene = presenter.build_scene(vec2f(800., 500.), 1., None, ctx); custom_endpoint_modal_height(&scene) }; assert!( expanded_modal_height > initial_modal_height, "expanded modal height {expanded_modal_height} should be greater than initial modal height {initial_modal_height}" ); assert!( (expanded_modal_height - 765.).abs() < 0.1, "expanded modal height {expanded_modal_height} should reach the 80% window-height cap" ); assert!( small_window_height < expanded_modal_height, "small modal height {small_window_height} should be less than expanded modal height {expanded_modal_height}" ); assert!( (small_window_height - 365.).abs() < 0.1, "small modal height {small_window_height} should reach the 80% window-height cap" ); }); }) } #[test] fn modal_with_many_models_lays_out() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(20); let (window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); assert_eq!(modal.as_ref(ctx).model_rows.len(), 20); }); }) } #[test] fn model_row_inputs_align_and_controls_fit_gutter() { assert_eq!(MODEL_INPUT_WIDTH * 2. + MODEL_ROW_SPACING, INPUT_WIDTH); // SCROLL_CONTENT_RIGHT_MARGIN already includes MODAL_SCROLLBAR_WIDTH, so the // right gutter (button spacing + remove-button column + content right margin) // is 56 without adding the scrollbar width again. assert_eq!( REMOVE_MODEL_BUTTON_SPACING + REMOVE_MODEL_BUTTON_COL_WIDTH + SCROLL_CONTENT_RIGHT_MARGIN, 56. ); } #[test] fn action_row_remains_fixed_when_form_scrolls() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(20); let (window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { let body = ctx.add_typed_action_view(|ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); Modal::new(Some("Edit custom endpoint".to_string()), body, ctx) .with_modal_style(UiComponentStyles { width: Some(560.), ..Default::default() }) .with_max_height_percentage(0.8) }); let body = modal.read(&app, |modal, _| modal.body().clone()); let invalidation = WindowInvalidation { updated: EntityIdSet::from_iter([ app.root_view_id(window_id).expect("root view should exist"), body.id(), ]), ..Default::default() }; let action_row_position = app.update(|ctx| { let presenter = ctx.presenter(window_id).expect("presenter should exist"); let mut presenter = presenter.borrow_mut(); presenter.invalidate(invalidation.clone(), ctx); presenter.build_scene(vec2f(560., 600.), 1., None, ctx); presenter .position_cache() .get_position(ACTIONS_POSITION_ID) .expect("action row position should exist") }); body.update(&mut app, |body, ctx| { body.scroll_state.scroll_to(Pixels::new(f32::MAX)); ctx.notify(); }); let scrolled_action_row_position = app.update(|ctx| { let presenter = ctx.presenter(window_id).expect("presenter should exist"); let mut presenter = presenter.borrow_mut(); presenter.invalidate(invalidation, ctx); presenter.build_scene(vec2f(560., 600.), 1., None, ctx); presenter .position_cache() .get_position(ACTIONS_POSITION_ID) .expect("action row position should exist") }); assert!(body.read(&app, |body, _| body.scroll_state.scroll_start()) > Pixels::zero()); assert_eq!( action_row_position, scrolled_action_row_position, "action row should remain fixed while form content scrolls" ); }) } #[test] fn focus_editor_scrolls_whole_form_to_field() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(20); let (window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); }); modal.update(&mut app, |modal, ctx| { let editor = modal .model_rows .last() .expect("model row should exist") .name_editor .clone(); modal.focus_editor(&editor, ctx); }); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); assert!(modal.as_ref(ctx).scroll_state.scroll_start() > Pixels::zero()); }); let model_scroll_start = modal.read(&app, |modal, _| modal.scroll_state.scroll_start()); modal.update(&mut app, |modal, ctx| { modal.focus_editor(&modal.endpoint_name_editor.clone(), ctx); }); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); assert!(modal.as_ref(ctx).scroll_state.scroll_start() < model_scroll_start); }); }) } #[test] fn add_model_scrolls_only_after_form_is_full() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(1); let (window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); modal.update(&mut app, |modal, ctx| modal.add_model(ctx)); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); assert_eq!( modal.as_ref(ctx).scroll_state.scroll_start(), Pixels::zero() ); }); modal.update(&mut app, |modal, ctx| { for _ in 0..20 { modal.add_model(ctx); } assert_eq!(modal.scroll_state.scroll_start(), Pixels::new(f32::MAX)); }); app.update(|ctx| { ctx.presenter(window_id) .expect("presenter should exist") .borrow_mut() .build_scene(vec2f(560., 600.), 1., None, ctx); let scroll_start = modal.as_ref(ctx).scroll_state.scroll_start(); assert!(scroll_start > Pixels::zero()); assert!(scroll_start < Pixels::new(f32::MAX)); }); }) } #[test] fn prefill_resets_form_scroll_position() { App::test((), |mut app| async move { init_modal_test_models(&mut app); let endpoint = endpoint_with_models(20); let (_window_id, modal) = app.add_window(WindowStyle::NotStealFocus, move |ctx| { CustomEndpointModal::new(Some(&endpoint), Some(0), ctx) }); modal.update(&mut app, |modal, ctx| { modal.scroll_state.scroll_to(Pixels::new(100.)); assert_eq!(modal.scroll_state.scroll_start(), Pixels::new(100.)); modal.prefill(None, None, ctx); assert_eq!(modal.scroll_state.scroll_start(), Pixels::zero()); }); }) } #[test] fn validate_url_accepts_https_with_host() { assert!(validate_url("https://api.example.com/v1").is_ok()); assert!(validate_url("https://example.com").is_ok()); assert!(validate_url("https://8.8.8.8/v1").is_ok()); } #[test] fn validate_url_rejects_http() { assert_eq!( validate_url("http://api.example.com/v1"), Err("URL must use HTTPS") ); assert_eq!( validate_url("http://example.com"), Err("URL must use HTTPS") ); } #[test] fn validate_url_rejects_ftp_and_other_schemes() { assert_eq!( validate_url("ftp://files.example.com"), Err("URL must use HTTPS") ); assert_eq!( validate_url("file:///etc/passwd"), Err("URL must use HTTPS") ); assert_eq!( validate_url("ws://socket.example.com"), Err("URL must use HTTPS") ); } #[test] fn validate_url_rejects_malformed_strings() { assert_eq!(validate_url("not a url"), Err("Invalid URL")); assert_eq!(validate_url("https://"), Err("Invalid URL")); } #[test] fn validate_url_rejects_empty_host() { assert_eq!(validate_url("https://?query=1"), Err("Invalid URL")); } #[test] fn validate_url_allows_empty_string() { assert!(validate_url("").is_ok()); } #[test] fn validate_url_allows_whitespace_only() { assert!(validate_url(" ").is_ok()); } #[test] fn validate_url_rejects_localhost_and_private_ips() { let error = Err("URL must not use a local or private host"); assert_eq!(validate_url("https://localhost:8080"), error); assert_eq!(validate_url("https://127.0.0.1/v1"), error); assert_eq!(validate_url("https://0.0.0.0/v1"), error); assert_eq!(validate_url("https://10.0.0.1/v1"), error); assert_eq!(validate_url("https://172.16.0.1/v1"), error); assert_eq!(validate_url("https://192.168.0.1/v1"), error); assert_eq!(validate_url("https://169.254.0.1/v1"), error); assert_eq!(validate_url("https://[::1]/v1"), error); assert_eq!(validate_url("https://[::]/v1"), error); assert_eq!(validate_url("https://[fc00::1]/v1"), error); assert_eq!(validate_url("https://[fe80::1]/v1"), error); assert_eq!(validate_url("https://[::ffff:192.168.0.1]/v1"), error); } #[test] fn endpoint_form_valid_rejects_invalid_current_url() { assert!(!is_endpoint_form_valid( "Endpoint", "http://api.example.com/v1", "key", true )); } #[test] fn endpoint_form_valid_requires_non_empty_url() { assert!(!is_endpoint_form_valid("Endpoint", "", "key", true)); assert!(!is_endpoint_form_valid("Endpoint", " ", "key", true)); } #[test] fn endpoint_form_valid_accepts_complete_valid_form() { assert!(is_endpoint_form_valid( "Endpoint", "https://api.example.com/v1", "key", true )); }