366 lines
13 KiB
Rust
366 lines
13 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
|
use galaxy_core::ui::appearance::Appearance;
|
|
use galaxyui::elements::{ChildView, Empty};
|
|
use galaxyui::platform::WindowStyle;
|
|
use galaxyui::{App, AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
|
|
|
|
use super::*;
|
|
use crate::server::server_api::ServerApiProvider;
|
|
use crate::test_util::settings::initialize_settings_for_tests;
|
|
use crate::workspace::ToastStack;
|
|
|
|
fn init_modal_test_models(app: &mut App) {
|
|
initialize_settings_for_tests(app);
|
|
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
|
|
app.add_singleton_model(|_| Appearance::mock());
|
|
app.add_singleton_model(|_| ToastStack);
|
|
|
|
// The modal queries CodebaseIndexManager for locally indexed repos.
|
|
// Register a test instance so `available_indexed_repos(...)` doesn't panic.
|
|
app.add_singleton_model(|ctx| {
|
|
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
|
|
});
|
|
}
|
|
|
|
#[derive(Default)]
|
|
// Simple view that owns the modal and records its emitted events.
|
|
struct ModalHarness {
|
|
modal: Option<ViewHandle<AgentAssistedEnvironmentModal>>,
|
|
events: Vec<AgentAssistedEnvironmentModalEvent>,
|
|
}
|
|
|
|
impl ModalHarness {
|
|
fn new(ctx: &mut ViewContext<Self>) -> Self {
|
|
let modal = ctx.add_typed_action_view(AgentAssistedEnvironmentModal::new);
|
|
ctx.subscribe_to_view(&modal, |me, _, event, ctx| {
|
|
me.events.push(event.clone());
|
|
ctx.notify();
|
|
});
|
|
|
|
Self {
|
|
modal: Some(modal),
|
|
events: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn modal(&self) -> ViewHandle<AgentAssistedEnvironmentModal> {
|
|
self.modal
|
|
.clone()
|
|
.expect("ModalHarness.modal should be initialized")
|
|
}
|
|
}
|
|
|
|
impl Entity for ModalHarness {
|
|
type Event = ();
|
|
}
|
|
|
|
impl View for ModalHarness {
|
|
fn ui_name() -> &'static str {
|
|
"AgentAssistedEnvironmentModalTestHarness"
|
|
}
|
|
|
|
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
|
let Some(modal) = &self.modal else {
|
|
return Empty::new().finish();
|
|
};
|
|
|
|
ChildView::new(modal).finish()
|
|
}
|
|
}
|
|
|
|
impl TypedActionView for ModalHarness {
|
|
type Action = ();
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_default_render_is_empty() {
|
|
// When not visible, the modal renders as an Empty element.
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
harness.read(&app, |harness, ctx| {
|
|
let modal = harness.modal();
|
|
let element = modal.as_ref(ctx).render(ctx);
|
|
let text_content = element.debug_text_content().unwrap_or_default();
|
|
assert!(
|
|
text_content.is_empty(),
|
|
"Expected empty modal render when hidden, got: {}",
|
|
text_content
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_show_renders_expected_copy_with_empty_repos_message() {
|
|
// We validate the copy via the section renderers (selected/available) rather than the full dialog,
|
|
// because the dialog includes icons/buttons that may rely on asset providers in unit tests.
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
// Show modal, then force it into the non-loading empty state so the content is deterministic.
|
|
harness.update(&mut app, |harness, ctx| {
|
|
harness.events.clear();
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.show(ctx);
|
|
modal.stop_available_repos_loading();
|
|
modal.available_repos.clear();
|
|
});
|
|
});
|
|
|
|
harness.read(&app, |harness, ctx| {
|
|
let appearance = Appearance::as_ref(ctx);
|
|
let modal = harness.modal();
|
|
let modal = modal.as_ref(ctx);
|
|
|
|
let selected_section = modal.render_selected_section(appearance);
|
|
let selected_text = selected_section.debug_text_content().unwrap_or_default();
|
|
assert!(
|
|
selected_text.contains("Selected repos"),
|
|
"Expected selected section title in rendered content: {}",
|
|
selected_text
|
|
);
|
|
assert!(
|
|
selected_text.contains("No repos selected yet"),
|
|
"Expected selected empty-state message in rendered content: {}",
|
|
selected_text
|
|
);
|
|
|
|
let available_section = modal.render_available_section(appearance);
|
|
let available_text = available_section.debug_text_content().unwrap_or_default();
|
|
assert!(
|
|
available_text.contains("Available indexed repos"),
|
|
"Expected available section title in rendered content: {}",
|
|
available_text
|
|
);
|
|
assert!(
|
|
available_text
|
|
.contains("No locally indexed repos found yet. Index a repo, then try again."),
|
|
"Expected available empty-state message in rendered content: {}",
|
|
available_text
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_cancel_emits_event() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
harness.update(&mut app, |harness, ctx| {
|
|
harness.events.clear();
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.handle_action(&AgentAssistedEnvironmentModalAction::Cancel, ctx);
|
|
});
|
|
});
|
|
|
|
harness.read(&app, |harness, _ctx| {
|
|
assert!(
|
|
matches!(
|
|
harness.events.as_slice(),
|
|
[AgentAssistedEnvironmentModalEvent::Cancelled]
|
|
),
|
|
"Expected a single Cancelled event"
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_confirm_only_emits_event_when_repos_selected() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
// Confirm with no selection should not emit.
|
|
harness.update(&mut app, |harness, ctx| {
|
|
harness.events.clear();
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.selected_repo_paths.clear();
|
|
modal.selected_row_mouse_states.clear();
|
|
modal.handle_action(&AgentAssistedEnvironmentModalAction::Confirm, ctx);
|
|
});
|
|
});
|
|
|
|
harness.read(&app, |harness, _ctx| {
|
|
assert!(
|
|
harness.events.is_empty(),
|
|
"Did not expect any events when confirming with no selected repos"
|
|
);
|
|
});
|
|
|
|
// Confirm with a selected repo should emit Confirmed.
|
|
let selected_repo = PathBuf::from("/tmp/repo-a");
|
|
harness.update(&mut app, |harness, ctx| {
|
|
harness.events.clear();
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.selected_repo_paths = vec![selected_repo.clone()];
|
|
modal.selected_row_mouse_states = vec![MouseStateHandle::default()];
|
|
modal.handle_action(&AgentAssistedEnvironmentModalAction::Confirm, ctx);
|
|
});
|
|
});
|
|
|
|
harness.read(&app, |harness, _ctx| {
|
|
assert!(
|
|
matches!(
|
|
harness.events.as_slice(),
|
|
[AgentAssistedEnvironmentModalEvent::Confirmed { repo_paths }]
|
|
if repo_paths == &vec!["/tmp/repo-a".to_string()]
|
|
),
|
|
"Expected a single Confirmed event with selected repo path"
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_show_clears_selection() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
harness.update(&mut app, |harness, ctx| {
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.selected_repo_paths = vec![PathBuf::from("/tmp/should-clear")];
|
|
modal.selected_row_mouse_states = vec![MouseStateHandle::default()];
|
|
|
|
modal.show(ctx);
|
|
|
|
assert!(modal.selected_repo_paths.is_empty());
|
|
assert!(modal.selected_row_mouse_states.is_empty());
|
|
});
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_directory_picked_adds_repo_and_confirm_emits_event() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
let tmp_dir = tempfile::TempDir::new().expect("TempDir should be creatable");
|
|
git2::Repository::init(tmp_dir.path()).expect("git repo should be init-able");
|
|
let selected_repo = tmp_dir.path().to_path_buf();
|
|
let expected_repo_string = dunce::canonicalize(tmp_dir.path())
|
|
.unwrap_or_else(|_| tmp_dir.path().to_path_buf())
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
harness.update(&mut app, |harness, ctx| {
|
|
harness.events.clear();
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.show(ctx);
|
|
modal.stop_available_repos_loading();
|
|
|
|
modal.handle_action(
|
|
&AgentAssistedEnvironmentModalAction::DirectoryPicked(
|
|
Ok(selected_repo.clone()),
|
|
),
|
|
ctx,
|
|
);
|
|
modal.handle_action(&AgentAssistedEnvironmentModalAction::Confirm, ctx);
|
|
});
|
|
});
|
|
|
|
harness.read(&app, |harness, _ctx| {
|
|
let [AgentAssistedEnvironmentModalEvent::Confirmed { repo_paths }] =
|
|
harness.events.as_slice()
|
|
else {
|
|
panic!(
|
|
"Expected a single Confirmed event with selected repo path, got: {:?}",
|
|
harness.events
|
|
);
|
|
};
|
|
|
|
assert_eq!(repo_paths.len(), 1);
|
|
let actual = dunce::canonicalize(PathBuf::from(&repo_paths[0]))
|
|
.unwrap_or_else(|_| PathBuf::from(&repo_paths[0]))
|
|
.to_string_lossy()
|
|
.to_string();
|
|
assert_eq!(actual, expected_repo_string);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_directory_picked_dedupes_paths() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
let tmp_dir = tempfile::TempDir::new().expect("TempDir should be creatable");
|
|
git2::Repository::init(tmp_dir.path()).expect("git repo should be init-able");
|
|
let selected_repo = tmp_dir.path().to_path_buf();
|
|
|
|
harness.update(&mut app, |harness, ctx| {
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.show(ctx);
|
|
|
|
modal.handle_action(
|
|
&AgentAssistedEnvironmentModalAction::DirectoryPicked(
|
|
Ok(selected_repo.clone()),
|
|
),
|
|
ctx,
|
|
);
|
|
modal.handle_action(
|
|
&AgentAssistedEnvironmentModalAction::DirectoryPicked(
|
|
Ok(selected_repo.clone()),
|
|
),
|
|
ctx,
|
|
);
|
|
|
|
assert_eq!(modal.selected_repo_paths.len(), 1);
|
|
assert_eq!(modal.selected_row_mouse_states.len(), 1);
|
|
});
|
|
});
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_modal_directory_picked_rejects_non_repos() {
|
|
App::test((), |mut app| async move {
|
|
init_modal_test_models(&mut app);
|
|
|
|
let (_window_id, harness) = app.add_window(WindowStyle::NotStealFocus, ModalHarness::new);
|
|
|
|
let tmp_dir = tempfile::TempDir::new().expect("TempDir should be creatable");
|
|
let selected_dir = tmp_dir.path().to_path_buf();
|
|
|
|
harness.update(&mut app, |harness, ctx| {
|
|
let modal = harness.modal();
|
|
modal.update(ctx, |modal, ctx| {
|
|
modal.show(ctx);
|
|
|
|
modal.handle_action(
|
|
&AgentAssistedEnvironmentModalAction::DirectoryPicked(Ok(selected_dir.clone())),
|
|
ctx,
|
|
);
|
|
|
|
assert!(modal.selected_repo_paths.is_empty());
|
|
assert!(modal.selected_row_mouse_states.is_empty());
|
|
});
|
|
});
|
|
})
|
|
}
|