use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use super::*;
use crate::app::{Modal, ProjectOpenMode, TextBuffer, View};
use crate::git::support::same_path;
use crate::git::tests::TestRepository;
use crate::ssh::SshMachine;
fn test_repository(branch: &str) -> (TestRepository, Repository) {
let fixture = TestRepository::with_branch(branch);
let repository = fixture.repository();
(fixture, repository)
}
fn workspace(repository: &Repository) -> RepositoryWorkspace {
RepositoryWorkspace::new(
repository,
ThemeName::Quinjet,
AppearanceChoice::Dark,
false,
false,
None,
)
}
#[test]
fn machine_session_restores_open_tabs_in_order_and_reactivates_the_saved_project() {
let (_first_directory, first_repository) = test_repository("first");
let (_second_directory, second_repository) = test_repository("second");
let session = ProjectSession {
roots: vec![
first_repository.root().to_path_buf(),
PathBuf::from("/missing/project"),
second_repository.root().to_path_buf(),
],
active: Some(first_repository.root().to_path_buf()),
};
let mut restored = RepositoryWorkspace::restore(
&session,
ThemeName::Quinjet,
AppearanceChoice::Dark,
false,
false,
None,
)
.expect("restored workspace");
let tabs = restored.tabs.infos();
assert_eq!(tabs.len(), 2);
assert!(
tabs.first()
.is_some_and(|tab| { tab.active && same_path(&tab.root, first_repository.root()) })
);
assert!(
tabs.get(1)
.is_some_and(|tab| { !tab.active && same_path(&tab.root, second_repository.root()) })
);
let restored_session = restored.project_session();
assert_eq!(restored_session.roots.len(), 2);
assert!(
restored_session
.active
.as_deref()
.is_some_and(|active| same_path(active, first_repository.root()))
);
assert_eq!(restored.initial_effects().len(), 2);
}
#[test]
fn machine_picker_context_follows_replaced_and_appended_projects() {
let (_first_directory, first_repository) = test_repository("first");
let (_second_directory, second_repository) = test_repository("second");
let context = SshContext {
current: "local".to_owned(),
machines: vec![SshMachine {
target: "remote-host".to_owned(),
folder: "/work/remote".into(),
accessible: true,
uses: 5,
local: false,
}],
};
let mut workspace = RepositoryWorkspace::new(
&first_repository,
ThemeName::Quinjet,
AppearanceChoice::Dark,
false,
false,
Some(context.clone()),
);
let now = Instant::now();
let first = workspace.active_id().expect("first tab is active");
let second = workspace
.append_repository(first, &second_repository, now)
.expect("append second repository")
.id;
assert_eq!(
workspace
.app_mut(first)
.and_then(|app| app.ssh_context.clone()),
Some(context.clone())
);
assert_eq!(
workspace
.app_mut(second)
.and_then(|app| app.ssh_context.clone()),
Some(context)
);
}
#[test]
fn activating_real_repository_tabs_restores_each_apps_state() {
let (_first_directory, first_repository) = test_repository("alpha");
let (_second_directory, second_repository) = test_repository("beta");
let mut workspace = workspace(&first_repository);
let now = Instant::now();
let first = workspace.active_id().expect("first tab is active");
let second = workspace
.append_repository(first, &second_repository, now)
.expect("append second repository")
.id;
let first_app = workspace.app_mut(first).expect("first app");
first_app.view = View::History;
first_app.history_cursor = 4;
first_app.content_scroll = 31;
let second_app = workspace.app_mut(second).expect("second app");
second_app.view = View::PullRequests;
second_app.content_scroll = 73;
workspace.activate(first, now);
let first_app = workspace.active_app_mut().expect("first app is active");
assert_eq!(first_app.repository_root, first_repository.root());
assert_eq!(first_app.view, View::History);
assert_eq!(first_app.history_cursor, 4);
assert_eq!(first_app.content_scroll, 31);
workspace.activate(second, now);
let second_app = workspace.active_app_mut().expect("second app is active");
assert_eq!(second_app.repository_root, second_repository.root());
assert_eq!(second_app.view, View::PullRequests);
assert_eq!(second_app.content_scroll, 73);
assert_eq!(
workspace.app_mut(first).map(|app| app.content_scroll),
Some(31)
);
}
#[test]
fn replacement_keeps_tab_identity_and_new_tabs_follow_the_close_lifecycle() {
let (_first_directory, first_repository) = test_repository("first");
let (_second_directory, second_repository) = test_repository("second");
let (_replacement_directory, replacement_repository) = test_repository("replacement");
let mut workspace = workspace(&first_repository);
let now = Instant::now();
let first = workspace.active_id().expect("first tab is active");
let second_effects = workspace
.append_repository(first, &second_repository, now)
.expect("append second repository");
let second = second_effects.id;
assert_ne!(first, second);
assert_eq!(workspace.active_id(), Some(second));
assert_eq!(workspace.tabs.len(), 2);
workspace.activate(first, now);
let replacement_effects = workspace
.replace_repository(first, &replacement_repository, now)
.expect("replace first repository");
assert_eq!(replacement_effects.id, first);
assert_eq!(workspace.active_id(), Some(first));
assert_eq!(workspace.tabs.len(), 2);
assert_eq!(workspace.tabs.id_for_root(first_repository.root()), None);
assert_eq!(
workspace.tabs.id_for_root(replacement_repository.root()),
Some(first)
);
assert_eq!(
workspace
.tabs
.infos()
.into_iter()
.map(|tab| tab.id)
.collect::<Vec<_>>(),
vec![first, second]
);
assert!(workspace.close(first, now));
assert_eq!(workspace.active_id(), Some(second));
assert_eq!(workspace.tabs.len(), 1);
assert!(!workspace.close(second, now));
assert!(workspace.tabs.is_empty());
}
#[test]
fn failed_project_open_returns_to_the_picker() {
let (_directory, repository) = test_repository("first");
let mut workspace = workspace(&repository);
let source = workspace.active_id().expect("active tab");
workspace.app_mut(source).expect("source app").modal = Some(Modal::Projects {
groups: Vec::new(),
selected: 0,
query: TextBuffer::default(),
collapsed: HashSet::new(),
loading: false,
opening: Some("/missing/project".into()),
mode: ProjectOpenMode::CurrentTab,
});
assert!(
workspace
.switch_repository(source, Path::new("/missing/project"), Instant::now())
.is_none()
);
assert!(matches!(
workspace.app_mut(source).and_then(|app| app.modal.as_ref()),
Some(Modal::Projects { opening: None, .. })
));
}
#[test]
fn worker_events_update_only_the_repository_tab_that_owns_them() {
let (_first_directory, first_repository) = test_repository("worker-alpha");
let (_second_directory, second_repository) = test_repository("worker-beta");
let mut workspace = workspace(&first_repository);
let now = Instant::now();
let first = workspace.active_id().expect("first tab is active");
let second = workspace
.append_repository(first, &second_repository, now)
.expect("append second repository")
.id;
workspace
.app_mut(first)
.expect("first app")
.status_generation = 101;
workspace
.app_mut(second)
.expect("second app")
.status_generation = 202;
assert!(workspace.send(first, WorkerCommand::Refresh { generation: 101 }));
assert!(workspace.send(second, WorkerCommand::Refresh { generation: 202 }));
let deadline = Instant::now() + Duration::from_secs(5);
let mut routed = HashSet::new();
loop {
routed.extend(
workspace
.drain_worker_events(Instant::now())
.into_iter()
.map(|effects| effects.id),
);
let first_branch = workspace
.app_mut(first)
.map(|app| app.status.branch.head.clone());
let second_branch = workspace
.app_mut(second)
.map(|app| app.status.branch.head.clone());
if first_branch.as_deref() == Some("worker-alpha")
&& second_branch.as_deref() == Some("worker-beta")
{
break;
}
assert!(
Instant::now() < deadline,
"workers did not return repository-specific status before the deadline"
);
thread::sleep(Duration::from_millis(5));
}
assert_eq!(routed, HashSet::from([first, second]));
assert_eq!(
workspace
.app_mut(first)
.map(|app| app.status.branch.head.as_str()),
Some("worker-alpha")
);
assert_eq!(
workspace
.app_mut(second)
.map(|app| app.status.branch.head.as_str()),
Some("worker-beta")
);
}