//! Integration tests for bootstrapping logic. use galaxy::{ cmd_or_ctrl_shift, integration_testing::{ input::{ input_contains_string, input_editor_is_focused, input_editor_is_not_focused, input_is_empty, }, step::new_step_with_default_assertions, tab::tab_title_step, terminal::{ assert_active_block_command_for_single_terminal_in_tab, assert_long_running_block_executing_for_single_terminal_in_tab, execute_command_for_single_terminal_in_tab, util::{current_shell_starter_and_version, ExpectedExitStatus}, wait_until_bootstrapped_single_pane_for_tab, }, view_getters::{single_input_view_for_tab, single_terminal_view_for_tab}, }, terminal::session_settings::HonorPS1, terminal::shell::{self, ShellType}, workspace::Workspace, }; use galaxyui::{ async_assert, async_assert_eq, clipboard::ClipboardContent, integration::TestStep, ViewHandle, }; use settings::Setting as _; use version_compare::Cmp; use crate::util::{write_all_rc_files_for_test, write_rc_files_for_test, ShellRcType}; use super::{new_builder, Builder}; /// Ensures that config files are only sourced once when bootstrapping a new session. pub fn test_rc_files_only_sourced_once_during_bootstrapping() -> Builder { new_builder() .with_setup(|utils| { let dir = utils.test_dir(); write_all_rc_files_for_test(dir, r"echo 'foo' >> ~/rc_output"); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Currently, when starting the application, we invoke a one-off non-interactive, login // shell that is not _associated_ to any particular session (see `LocalShell::new`). // Since this test only cares about the fact that the config files are sourced // once _per session_, we clear the effects of this one-off initialization // and then ensure that starting a session only sources config files once. .with_step(execute_command_for_single_terminal_in_tab( 0, "rm ~/rc_output".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Add a new session") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) .with_step(execute_command_for_single_terminal_in_tab( 1, "cat ~/rc_output".to_string(), ExpectedExitStatus::Success, "foo", )) } pub fn test_unescaped_prompt_bootstraps() -> Builder { new_builder() .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r"export PATH=/Applications/VMware\\\ Fusion.app/Contents/Public:$PATH", [ShellRcType::Bash, ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) } // TODO: test doesn't work on fish because no block is created from output of config.fish pub fn test_paste_and_type_characters_before_bootstrap() -> Builder { new_builder() .set_should_run_test(|| { let (starter, _version) = current_shell_starter_and_version(); !matches!(starter.shell_type(), ShellType::Fish) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test(&dir, "echo -n 'Enter some user input: ' && read", [ShellRcType::Zsh, ShellRcType::Bash, ShellRcType::Fish]); write_rc_files_for_test(&dir, "Read-Host 'Enter some user input'", [ShellRcType::PowerShell]); // On Ubuntu (and possibly other Linux distros), a message is // printed out during shell initialization telling the user how to // use `sudo`. This interferes with our expected pty contents, so suppress the message. if cfg!(target_os = "linux") { std::fs::File::create(dir.join(".sudo_as_admin_successful")) .expect("should not fail to create file in home directory"); } }) .with_step( TestStep::new("Wait for rc file to run") .add_named_assertion("Long running block executing", assert_long_running_block_executing_for_single_terminal_in_tab(false, 0)) // Output pre-bootstrap writes to the block's command, not the output, as nothing causes // us to finish the command grid. .add_named_assertion("Validate block contents", assert_active_block_command_for_single_terminal_in_tab("Enter some user input: ", 0)) ) .with_step( TestStep::new("Warp input should not start focused, since .rc file is reading user input") .add_assertion(input_editor_is_not_focused(0)) ) .with_step( TestStep::new("Populate the clipboard") .with_action(|app, _, _| { app.update(|app| { app.clipboard().write(ClipboardContent::plain_text("this is the pasted text".to_string())) }) }) .add_assertion(|app, _| { app.update(|app| { async_assert_eq!( app.clipboard().read().plain_text, String::from("this is the pasted text"), "Clipboard should be populated", ) }) }) // Make sure the input is not focused before we paste, even though we checked this in an earlier step. // The input is sometimes focused for a brief moment causing the paste to go to the wrong place without this check for some reason. .add_assertion(input_editor_is_not_focused(0)), ) .with_step( TestStep::new("Pasted text go into the pty and not warp input") .with_keystrokes(&[cmd_or_ctrl_shift("v")]) .add_named_assertion("Input should be empty", input_is_empty(0)) .add_named_assertion("Pasted text should go to pty", |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _| { async_assert_eq!( view.model.lock().block_list().active_block().command_to_string(), String::from("Enter some user input: this is the pasted text"), "Paste should go to pty" ) }) }) // Make sure the input is not focused before we type, even though we checked this in an earlier step. // The input is sometimes focused for a brief moment causing typed characters to go to the wrong place without this check for some reason. .add_named_assertion("Input should not be focused", input_editor_is_not_focused(0)), ) .with_step( TestStep::new("Typed characters should go to the pty and not warp input") .with_typed_characters(&["these are some typed characters"]) .add_named_assertion("Input should be empty", input_is_empty(0)) .add_named_assertion("Typed characters should go to pty", |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _| { async_assert_eq!( view.model.lock().block_list().active_block().command_to_string(), String::from("Enter some user input: this is the pasted textthese are some typed characters"), "Typed characters should go to pty" ) }) }) ) .with_step( TestStep::new("Click on input to focus the input box") .with_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); input.read(app, |input, _| { input.save_position_id() }) }) .add_assertion(input_editor_is_focused(0)), ) .with_step( TestStep::new("Pasted text should go in input since input is focused") .with_keystrokes(&[cmd_or_ctrl_shift("v")]) .add_assertion(input_contains_string(0, "this is the pasted text".to_owned())) ) .with_step( TestStep::new("Typed characters should go in input since input is focused") .with_typed_characters(&["these are some typed characters"]) .add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned())), ) .with_step( TestStep::new("Focus the terminal view") .with_click_on_saved_position_fn(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _| { view.content_element_position_id().to_owned() }) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let focused_view_id = app.focused_view_id(window_id).expect("Focused view should exist"); async_assert!(focused_view_id == terminal_view.id(), "Terminal should be focused") }), ) .with_step( TestStep::new("Press enter to finish user input and allow bootstrapping to finish") .with_keystrokes(&["enter"]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Warp input should be focused and keep buffered text") .add_assertion(input_editor_is_focused(0)) .add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned())) ) } pub fn test_bootstrap_with_no_script_execution_block() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Ensure there are no visible blocks") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let workspace = views.first().unwrap(); let terminal_view = workspace.read(app, |workspace, _| { workspace .get_pane_group_view_unchecked(0) .read(app, |pane_group, ctx| { pane_group .terminal_view_at_pane_index(0, ctx) .expect("View should be defined at pane 0") .clone() }) }); terminal_view.read(app, |view, _| { async_assert!( view.selected_blocks_tail_index().is_none(), "There should not be any blocks the user can select" ) }) }), ) } pub fn test_instant_prompt_bootstrap() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on zsh let (starter, _) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Zsh) }) .with_setup(|utils| { // If the instant prompt var is not set to off, hang forever. let dir = utils.test_dir(); write_rc_files_for_test( dir, r#"if [[ "$POWERLEVEL9K_INSTANT_PROMPT" != "off" ]]; then read; fi"#, [ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) } /// Ensure this issue doesn't happen again. /// https://github.com/warpdotdev/Warp/issues/2636 /// Bootstrapping was failing when PROMPT_COMMAND was an array pub fn test_bash_bootstraps_with_prompt_command_array() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash let (starter, version) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Bash) && version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" PROMPT_COMMAND=('printf "\033]0;TEST_TAB_TITLE\a"' 'echo hello') "#, [ShellRcType::Bash], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, /*tab_idx*/ "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step(tab_title_step( "Assert the user's tab title used", "TEST_TAB_TITLE".to_string(), )) } /// Ensures that the zsh bootstrap script's precmd/preexec hooks work correctly when the user /// has `setopt nounset` (set -u) enabled. This option causes the shell to error when referencing /// unset variables, so we need to use `${VAR:-}` syntax for external environment variables. /// /// The test enables nounset after bootstrap completes, then runs commands to trigger the /// precmd and preexec hooks which reference various environment variables. pub fn test_zsh_bootstraps_with_nounset_option() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on zsh let (starter, _) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Zsh) }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Enable nounset after bootstrap, then run a command to trigger precmd/preexec hooks .with_step(execute_command_for_single_terminal_in_tab( 0, "setopt nounset".to_string(), ExpectedExitStatus::Success, (), )) // Run another command to verify precmd/preexec work with nounset enabled .with_step(execute_command_for_single_terminal_in_tab( 0, "echo 'nounset test passed'".to_string(), ExpectedExitStatus::Success, "nounset test passed", )) } pub fn test_bash_bootstraps_with_prompt_command_array_that_sets_ps1() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash let (starter, version) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Bash) && version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false) }) .with_user_defaults(std::collections::HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" function custom_prompt() { PS1="darmok@tanagra$ " } PROMPT_COMMAND=('printf "\033]0;SHAKA WHEN THE WALLS FELL\a"' 'custom_prompt') "#, [ShellRcType::Bash], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, /*tab_idx*/ "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step(tab_title_step( "Assert the user's tab title used", "SHAKA WHEN THE WALLS FELL".to_string(), )) .with_step( new_step_with_default_assertions("Check PS1 value").add_assertion( move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let prompt = terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); let block = model .block_list() .blocks() .last() .expect("After bootstrapping, we should have a block"); block.prompt_to_string() }); async_assert_eq!( prompt, "darmok@tanagra$ ", "prompt should be 'darmok@tanagra$ ' but got '{prompt}' instead" ) }, ), ) }