use super::{ EnvironmentFormInitArgs, EnvironmentFormValues, GithubAuthRedirectTarget, SuggestImageState, UpdateEnvironmentForm, UpdateEnvironmentFormAction, }; use crate::ai::ambient_agents::github_auth_notifier::GitHubAuthNotifier; use crate::ai::cloud_environments::GithubRepo; use crate::auth::AuthStateProvider; use crate::cloud_object::model::persistence::CloudModel; use crate::network::NetworkStatus; use crate::server::ids::{ClientId, SyncId}; use crate::server::server_api::ServerApiProvider; use crate::server::{cloud_objects::update_manager::UpdateManager, sync_queue::SyncQueue}; use crate::settings::PrivacySettings; use crate::settings_view::keybindings::KeybindingChangedNotifier; use crate::test_util::settings::initialize_settings_for_tests; use crate::workspaces::team::Team; use crate::workspaces::team_tester::TeamTesterStatus; use crate::workspaces::user_workspaces::UserWorkspaces; use crate::workspaces::workspace::Workspace; use url::Url; use galaxy_core::ui::appearance::Appearance; use galaxyui::elements::{Empty, MouseStateHandle}; use galaxyui::platform::WindowStyle; use galaxyui::{ AddSingletonModel, App, AppContext, Element, Entity, SingletonEntity, TypedActionView, View, WindowId, }; #[test] fn test_parse_repo_input_owner_repo() { let (owner, repo) = UpdateEnvironmentForm::parse_repo_input("owner/repo") .expect("expected owner/repo to parse"); assert_eq!(owner, "owner"); assert_eq!(repo, "repo"); } #[test] fn test_parse_repo_input_github_url() { let (owner, repo) = UpdateEnvironmentForm::parse_repo_input("https://github.com/warp/warp.git") .expect("expected github url to parse"); assert_eq!(owner, "warp"); assert_eq!(repo, "warp"); } #[test] fn test_parse_repo_inputs_multiple_entries() { let parsed = UpdateEnvironmentForm::parse_repo_inputs( "https://github.com/warp/warp, warp/warp-internal\n git@github.com:warp/warp-server", ); assert_eq!( parsed, vec![ ("warp".to_string(), "warp".to_string()), ("warp".to_string(), "warp-internal".to_string()), ("warp".to_string(), "warp-server".to_string()), ] ); } #[test] fn test_parse_repo_inputs_invalid_returns_empty() { assert!(UpdateEnvironmentForm::parse_repo_inputs("not a repo").is_empty()); assert!(UpdateEnvironmentForm::parse_repo_inputs("owner/").is_empty()); assert!(UpdateEnvironmentForm::parse_repo_inputs("/repo").is_empty()); } #[test] fn test_build_auth_url_with_next_overrides_existing() { let base_url = "https://example.com/oauth/connect/github?foo=bar&next=old://settings/environments"; let result = UpdateEnvironmentForm::build_auth_url_with_next( base_url, GithubAuthRedirectTarget::SettingsEnvironments, "warpdev", ); let parsed = Url::parse(&result).expect("result should be valid url"); let mut next_values = parsed .query_pairs() .filter(|(key, _)| key == "next") .map(|(_, value)| value.into_owned()) .collect::>(); assert_eq!(next_values.len(), 1); assert_eq!( next_values.pop(), Some("warpdev://settings/environments".to_string()) ); assert!(parsed .query_pairs() .any(|(key, value)| key == "foo" && value == "bar")); } #[test] fn test_build_auth_url_with_next_focus_cloud_mode() { let base_url = "https://example.com/oauth/connect/github"; let result = UpdateEnvironmentForm::build_auth_url_with_next( base_url, GithubAuthRedirectTarget::FocusCloudMode, "warplocal", ); let parsed = Url::parse(&result).expect("result should be valid url"); let next_value = parsed .query_pairs() .find(|(key, _)| key == "next") .map(|(_, value)| value.into_owned()); assert_eq!( next_value, Some("warplocal://action/focus_cloud_mode".to_string()) ); } #[test] fn test_build_auth_url_with_next_uses_scheme_param() { let base_url = "https://example.com/oauth/connect/github?scheme=warp"; let result = UpdateEnvironmentForm::build_auth_url_with_next( base_url, GithubAuthRedirectTarget::FocusCloudMode, "warplocal", ); let parsed = Url::parse(&result).expect("result should be valid url"); let next_value = parsed .query_pairs() .find(|(key, _)| key == "next") .map(|(_, value)| value.into_owned()); assert_eq!( next_value, Some("warp://action/focus_cloud_mode".to_string()) ); } #[derive(Default)] struct TestRootView; impl Entity for TestRootView { type Event = (); } impl View for TestRootView { fn ui_name() -> &'static str { "TestRootView" } fn render(&self, _: &AppContext) -> Box { Empty::new().finish() } } impl TypedActionView for TestRootView { type Action = (); } fn create_test_window(app: &mut App) -> WindowId { let (window_id, _root_view) = app.add_window(WindowStyle::NotStealFocus, |_| TestRootView); window_id } fn init_update_environment_form_test_models(app: &mut App) { initialize_settings_for_tests(app); app.add_singleton_model(|_ctx| ServerApiProvider::new_for_test()); app.add_singleton_model(|_| AuthStateProvider::new_for_test()); app.add_singleton_model(|_| Appearance::mock()); app.add_singleton_model(CloudModel::mock); app.add_singleton_model(UserWorkspaces::default_mock); app.add_singleton_model(|_| NetworkStatus::new()); // These are required by some settings/UI code paths and by UserWorkspaces::update_workspaces. app.add_singleton_model(PrivacySettings::mock); app.add_singleton_model(TeamTesterStatus::mock); app.add_singleton_model(SyncQueue::mock); app.add_singleton_model(UpdateManager::mock); app.add_singleton_model(|_| KeybindingChangedNotifier::new()); app.add_singleton_model(|_| GitHubAuthNotifier::new()); } #[derive(Debug)] enum GithubAuthCallState { Loading, Authed, Unauthed { auth_url: String }, Error { message: String }, } impl GithubAuthCallState { fn unauthed(auth_url: impl Into) -> Self { Self::Unauthed { auth_url: auth_url.into(), } } fn error(message: impl Into) -> Self { Self::Error { message: message.into(), } } } fn set_github_auth_call_state(form: &mut UpdateEnvironmentForm, state: GithubAuthCallState) { match state { GithubAuthCallState::Loading => { form.github_dropdown_state.is_loading = true; form.github_dropdown_state.auth_url = None; form.github_dropdown_state.load_error_message = None; } GithubAuthCallState::Authed => { form.github_dropdown_state.is_loading = false; form.github_dropdown_state.auth_url = None; form.github_dropdown_state.load_error_message = None; } GithubAuthCallState::Unauthed { auth_url } => { form.github_dropdown_state.is_loading = false; form.github_dropdown_state.auth_url = Some(auth_url); form.github_dropdown_state.load_error_message = None; } GithubAuthCallState::Error { message } => { form.github_dropdown_state.is_loading = false; form.github_dropdown_state.auth_url = None; form.github_dropdown_state.load_error_message = Some(message); } } } fn team_for_test() -> Team { Team { uid: 123.into(), name: "test".to_string(), invite_code: None, members: vec![], pending_email_invites: vec![], invite_link_domain_restrictions: vec![], billing_metadata: Default::default(), stripe_customer_id: None, organization_settings: Default::default(), is_eligible_for_discovery: false, has_billing_history: false, } } fn workspace_for_test(team: &Team) -> Workspace { Workspace { uid: "workspace_uid123456789".to_string().into(), name: "test".to_string(), stripe_customer_id: None, teams: vec![team.clone()], billing_metadata: Default::default(), bonus_grants_purchased_this_month: Default::default(), has_billing_history: false, settings: Default::default(), invite_code: None, invite_link_domain_restrictions: vec![], pending_email_invites: vec![], is_eligible_for_discovery: false, members: vec![], total_requests_used_since_last_refresh: 0, } } #[test] fn test_environment_form_values_default() { let form_state = EnvironmentFormValues::default(); assert!(form_state.name.is_empty()); assert!(form_state.description.is_empty()); assert!(form_state.docker_image.is_empty()); assert!(form_state.setup_commands.is_empty()); assert!(form_state.selected_repos.is_empty()); } #[test] fn test_edit_mode_initializes_form_state_from_initial_values() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let env_id = SyncId::ClientId(ClientId::new()); let initial_values = EnvironmentFormValues { name: "Test Environment".to_string(), description: "A test environment for front end agents".to_string(), selected_repos: vec![ GithubRepo::new("owner1".to_string(), "repo1".to_string()), GithubRepo::new("owner2".to_string(), "repo2".to_string()), ], docker_image: "python:3.11".to_string(), setup_commands: vec![ "pip install -r requirements.txt".to_string(), "pytest".to_string(), ], }; let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test( EnvironmentFormInitArgs::Edit { env_id, initial_values: Box::new(initial_values.clone()), }, ctx, ) }); let form = view_handle.as_ref(ctx); assert_eq!(form.form_state.name, initial_values.name); assert_eq!(form.form_state.description, initial_values.description); assert_eq!(form.form_state.docker_image, initial_values.docker_image); assert_eq!( form.form_state.setup_commands, initial_values.setup_commands ); assert_eq!( form.form_state.selected_repos, initial_values.selected_repos ); }); }) } #[test] fn test_submit_button_disabled_until_required_fields_present() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); let mut view_handle = None; app.update(|ctx| { view_handle = Some(ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) })); }); let view_handle = view_handle.expect("UpdateEnvironmentForm handle should be created"); // Initial create page: missing required fields -> disabled. app.update(|ctx| { let is_disabled = view_handle .as_ref(ctx) .submit_button .read(ctx, |button, _| button.is_disabled()); assert!(is_disabled, "Expected submit button disabled initially"); }); // Only name set -> still disabled. app.update(|ctx| { view_handle.update(ctx, |form, ctx| { form.form_state.name = "My Env".to_string(); form.update_button_state(ctx); }); let is_disabled = view_handle .as_ref(ctx) .submit_button .read(ctx, |button, _| button.is_disabled()); assert!( is_disabled, "Expected submit button disabled without docker image" ); }); // Name + docker image set -> enabled. app.update(|ctx| { view_handle.update(ctx, |form, ctx| { form.form_state.docker_image = "ubuntu:latest".to_string(); form.update_button_state(ctx); }); let is_disabled = view_handle .as_ref(ctx) .submit_button .read(ctx, |button, _| button.is_disabled()); assert!( !is_disabled, "Expected submit button enabled when required fields set" ); }); }) } #[test] fn test_render_repos_field_loading_state() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { set_github_auth_call_state(form, GithubAuthCallState::Loading); }); let appearance = Appearance::as_ref(ctx); let element = view_handle.as_ref(ctx).render_repos_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Repo(s)"), "Expected 'Repo(s)' label in rendered content: {text_content}" ); assert!( text_content.contains("Loading..."), "Expected 'Loading...' in rendered content: {text_content}" ); }); }) } #[test] fn test_render_repos_field_authed_state() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { set_github_auth_call_state(form, GithubAuthCallState::Authed); }); let appearance = Appearance::as_ref(ctx); let element = view_handle.as_ref(ctx).render_repos_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Repo(s)"), "Expected 'Repo(s)' label in rendered content: {text_content}" ); assert!( text_content.contains("Type owner/repo and press Enter"), "Expected helper text in rendered content: {text_content}" ); }); }) } #[test] fn test_render_repos_field_auth_required() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { set_github_auth_call_state( form, GithubAuthCallState::unauthed("https://github.com/login/oauth/authorize"), ); }); let appearance = Appearance::as_ref(ctx); let element = view_handle.as_ref(ctx).render_repos_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Repo(s)"), "Expected 'Repo(s)' label in rendered content: {text_content}" ); assert!( text_content.contains("Auth with GitHub"), "Expected 'Auth with GitHub' in rendered content: {text_content}" ); }); }) } #[test] fn test_render_repos_field_error_state() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { set_github_auth_call_state( form, GithubAuthCallState::error("Failed to load GitHub repositories"), ); }); let appearance = Appearance::as_ref(ctx); let element = view_handle.as_ref(ctx).render_repos_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Repo(s)"), "Expected 'Repo(s)' label in rendered content: {text_content}" ); assert!( text_content.contains("Failed to load GitHub repositories"), "Expected error message in rendered content: {text_content}" ); assert!( text_content.contains("Retry"), "Expected 'Retry' in rendered content: {text_content}" ); }); }) } #[test] fn test_render_repos_field_with_selected_repos() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { set_github_auth_call_state(form, GithubAuthCallState::Authed); form.form_state.selected_repos = vec![ GithubRepo::new("warpdotdev".to_string(), "warp-internal".to_string()), GithubRepo::new("facebook".to_string(), "react".to_string()), ]; form.remove_repo_mouse_states = vec![MouseStateHandle::default(), MouseStateHandle::default()]; }); let appearance = Appearance::as_ref(ctx); let element = view_handle.as_ref(ctx).render_repos_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Repo(s)"), "Expected 'Repo(s)' label in rendered content: {text_content}" ); assert!( text_content.contains("warpdotdev/warp-internal"), "Expected 'warpdotdev/warp-internal' in rendered content: {text_content}" ); assert!( text_content.contains("facebook/react"), "Expected 'facebook/react' in rendered content: {text_content}" ); }); }) } #[test] fn test_authed_repo_input_allows_arbitrary_repo() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); let mut view_handle = None; app.update(|ctx| { view_handle = Some(ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) })); }); let view_handle = view_handle.expect("UpdateEnvironmentForm handle should be created"); app.update(|ctx| { view_handle.update(ctx, |form, ctx| { set_github_auth_call_state(form, GithubAuthCallState::Authed); form.github_dropdown_state.available_repos = vec![GithubRepo::new("other".to_string(), "repo".to_string())]; form.repos_input = "owner/new-repo".to_string(); form.handle_action(&UpdateEnvironmentFormAction::AddRepo, ctx); }); let form = view_handle.as_ref(ctx); assert_eq!(form.form_state.selected_repos.len(), 1); assert_eq!(form.form_state.selected_repos[0].owner, "owner"); assert_eq!(form.form_state.selected_repos[0].repo, "new-repo"); assert!(form.repos_input.is_empty()); assert_eq!(form.remove_repo_mouse_states.len(), 1); }); }) } #[test] fn test_selected_repos_as_remote_repo_args_formats_owner_repo_strings() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { form.form_state.selected_repos = vec![ GithubRepo::new("warpdotdev".to_string(), "warp-internal".to_string()), GithubRepo::new("facebook".to_string(), "react".to_string()), ]; }); let form = view_handle.as_ref(ctx); let args = form.selected_repos_as_remote_repo_args(); assert_eq!( args, vec![ "warpdotdev/warp-internal".to_string(), "facebook/react".to_string(), ] ); }); }) } #[test] fn test_can_suggest_image_for_edit_requires_repos_modified() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); let env_id = SyncId::ClientId(ClientId::new()); let initial_values = EnvironmentFormValues { name: "Env".to_string(), description: "".to_string(), selected_repos: vec![GithubRepo::new( "warpdotdev".to_string(), "warp-internal".to_string(), )], docker_image: "ubuntu:latest".to_string(), setup_commands: vec![], }; app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test( EnvironmentFormInitArgs::Edit { env_id, initial_values: Box::new(initial_values), }, ctx, ) }); let form = view_handle.as_ref(ctx); assert!( !form.can_suggest_image_for_current_repos(), "Expected suggest-image to be disabled on edit until repos are modified" ); view_handle.update(ctx, |form, _| { form.edit_repos_modified = true; form.suggest_image_last_attempt_key = None; form.suggest_image_state = SuggestImageState::Idle; }); let form = view_handle.as_ref(ctx); assert!( form.can_suggest_image_for_current_repos(), "Expected suggest-image to be enabled on edit after repos have been modified" ); }); }) } #[test] fn test_can_suggest_image_for_create_does_not_require_repos_modified() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { form.form_state.selected_repos = vec![GithubRepo::new( "warpdotdev".to_string(), "warp-internal".to_string(), )]; form.edit_repos_modified = false; form.suggest_image_last_attempt_key = None; form.suggest_image_state = SuggestImageState::Idle; }); let form = view_handle.as_ref(ctx); assert!( form.can_suggest_image_for_current_repos(), "Expected suggest-image to be enabled on create when repos exist" ); }); }) } #[test] fn test_render_docker_image_field_shows_suggest_image_button_on_create() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); let appearance = Appearance::as_ref(ctx); let element = view_handle .as_ref(ctx) .render_docker_image_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Suggest image"), "Expected suggest-image button text in rendered content: {text_content}" ); }); }) } #[test] fn test_render_docker_image_field_shows_suggest_image_button_on_edit() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let env_id = SyncId::ClientId(ClientId::new()); let initial_values = EnvironmentFormValues { name: "Env".to_string(), description: "".to_string(), selected_repos: vec![], docker_image: "ubuntu:latest".to_string(), setup_commands: vec![], }; let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test( EnvironmentFormInitArgs::Edit { env_id, initial_values: Box::new(initial_values), }, ctx, ) }); let appearance = Appearance::as_ref(ctx); let element = view_handle .as_ref(ctx) .render_docker_image_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Suggest image"), "Expected suggest-image button text in rendered content: {text_content}" ); }); }) } #[test] fn test_render_docker_image_field_shows_generating_state() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { form.form_state.selected_repos = vec![GithubRepo::new( "warpdotdev".to_string(), "warp-internal".to_string(), )]; let key = form .selected_repos_key() .expect("Expected repos key to exist"); form.suggest_image_state = SuggestImageState::Loading { key }; }); let appearance = Appearance::as_ref(ctx); let element = view_handle .as_ref(ctx) .render_docker_image_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Generating"), "Expected generating state in rendered content: {text_content}" ); }); }) } #[test] fn test_render_docker_image_field_shows_custom_image_warning() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { form.form_state.selected_repos = vec![GithubRepo::new( "warpdotdev".to_string(), "warp-internal".to_string(), )]; let key = form .selected_repos_key() .expect("Expected repos key to exist"); form.suggest_image_state = SuggestImageState::Success { key, needs_custom_image: true, reason: "No matching base image".to_string(), }; }); let appearance = Appearance::as_ref(ctx); let element = view_handle .as_ref(ctx) .render_docker_image_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("custom Docker image"), "Expected custom image messaging in rendered content: {text_content}" ); assert!( text_content.contains("No matching base image"), "Expected reason text in rendered content: {text_content}" ); assert!( text_content.contains("Launch agent"), "Expected 'Launch agent' action in rendered content: {text_content}" ); }); }) } #[test] fn test_render_docker_image_field_shows_github_auth_required_message() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); view_handle.update(ctx, |form, _| { form.form_state.selected_repos = vec![GithubRepo::new( "warpdotdev".to_string(), "warp-internal".to_string(), )]; let key = form .selected_repos_key() .expect("Expected repos key to exist"); form.suggest_image_state = SuggestImageState::AuthRequired { key, auth_url: "https://github.com/login/oauth/authorize".to_string(), }; }); let appearance = Appearance::as_ref(ctx); let element = view_handle .as_ref(ctx) .render_docker_image_field(appearance); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains( "You need to grant access to your GitHub repos to suggest a Docker image" ), "Expected GitHub auth required message in rendered content: {text_content}" ); assert!( text_content.contains("Authenticate"), "Expected 'Authenticate' action in rendered content: {text_content}" ); }); }) } #[test] fn test_create_environment_form_with_team_can_toggle_share_with_team_and_renders_warning_when_disabled( ) { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let team = team_for_test(); let workspace = workspace_for_test(&team); let workspace_uid = workspace.uid; UserWorkspaces::handle(ctx).update(ctx, |user_workspaces, ctx| { user_workspaces.update_workspaces(vec![workspace], ctx); user_workspaces.set_current_workspace_uid(workspace_uid, ctx); }); let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); assert!( view_handle.as_ref(ctx).share_with_team, "Expected share_with_team to default to true when user has a team" ); let element = view_handle.as_ref(ctx).render(ctx); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains("Share with team"), "Expected 'Share with team' checkbox label in rendered content: {text_content}" ); assert!( !text_content.contains( "Personal environments cannot be used with external integrations or team API keys", ), "Did not expect the warning to render when share_with_team is enabled: {text_content}" ); view_handle.update(ctx, |view, ctx| { view.handle_action(&UpdateEnvironmentFormAction::ToggleShareWithTeam, ctx); }); assert!( !view_handle.as_ref(ctx).share_with_team, "Expected share_with_team to be disabled after toggle" ); let element = view_handle.as_ref(ctx).render(ctx); let text_content = element.debug_text_content().unwrap_or_default(); assert!( text_content.contains( "Personal environments cannot be used with external integrations or team API keys", ), "Expected the warning to render when share_with_team is disabled: {text_content}" ); }); }) } #[test] fn test_create_environment_form_without_team_does_not_render_checkbox_and_defaults_disabled() { App::test((), |mut app| async move { init_update_environment_form_test_models(&mut app); let window_id = create_test_window(&mut app); app.update(|ctx| { let view_handle = ctx.add_typed_action_view(window_id, |ctx| { UpdateEnvironmentForm::new_for_test(EnvironmentFormInitArgs::Create, ctx) }); assert!( !view_handle.as_ref(ctx).share_with_team, "Expected share_with_team to default to false when user has no team" ); let element = view_handle.as_ref(ctx).render(ctx); let text_content = element.debug_text_content().unwrap_or_default(); assert!( !text_content.contains("Share with team"), "Did not expect 'Share with team' checkbox label in rendered content: {text_content}" ); }); }) } #[test] fn test_parse_docker_hub_url_bare_owner_repo() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("warp/base-image"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); } #[test] fn test_parse_docker_hub_url_with_tag() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("warp/base-image:latest"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("warp/base-image:v1.2.3"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); } #[test] fn test_parse_docker_hub_url_with_digest() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("warp/base-image@sha256:abc123"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); } #[test] fn test_parse_docker_hub_url_explicit_docker_io() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("docker.io/warp/base-image"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("docker.io/warp/base-image:latest"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); } #[test] fn test_parse_docker_hub_url_explicit_index_docker_io() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("index.docker.io/warp/base-image"), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); } #[test] fn test_parse_docker_hub_url_official_image() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("python"), Some("https://hub.docker.com/_/python".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("python:3.11"), Some("https://hub.docker.com/_/python".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("node:20-alpine"), Some("https://hub.docker.com/_/node".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("golang"), Some("https://hub.docker.com/_/golang".to_string()) ); } #[test] fn test_parse_docker_hub_url_official_image_explicit_library_prefix() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("docker.io/library/python"), Some("https://hub.docker.com/_/python".to_string()) ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("index.docker.io/library/node:20"), Some("https://hub.docker.com/_/node".to_string()) ); } #[test] fn test_parse_docker_hub_url_other_registry_returns_none() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("ghcr.io/warp/base-image"), None ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("gcr.io/project/image"), None ); assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url("quay.io/owner/repo"), None ); } #[test] fn test_parse_docker_hub_url_empty_or_whitespace() { assert_eq!(UpdateEnvironmentForm::parse_docker_hub_url(""), None); assert_eq!(UpdateEnvironmentForm::parse_docker_hub_url(" "), None); } #[test] fn test_parse_docker_hub_url_trims_whitespace() { assert_eq!( UpdateEnvironmentForm::parse_docker_hub_url(" warp/base-image "), Some("https://hub.docker.com/r/warp/base-image".to_string()) ); }