//! Note that for all of these tests, you need to also update //! src/integration.rs and src/bin/integration.rs in order to register them //! to be run. mod agent_mode; mod ai_assistant; mod ai_document; mod block_filtering; mod bootstrapping; mod code_review; mod ctrl_d; mod file_tree; mod goto_line; mod history; mod input; mod keyboard_protocol; mod launch_configs; mod notebooks; mod pane_restoration; #[cfg(target_os = "macos")] mod preview_config_migration; mod remote_server; mod rich_input_ctrl_enter; mod rules; mod secrets; mod session_restoration; mod settings_file_errors; mod settings_file_hot_reload; mod settings_file_migration; mod settings_private; mod ssh; mod subshell; mod sync_inputs; mod typeahead; mod video_recording; mod websockets; mod workflows; mod workspace; use std::borrow::Cow; use std::collections::HashMap; use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; pub use agent_mode::*; pub use ai_assistant::*; pub use ai_document::*; use anyhow::{anyhow, Result}; pub use block_filtering::*; pub use bootstrapping::*; pub use code_review::*; pub use ctrl_d::*; pub use file_tree::*; use float_cmp::assert_approx_eq; use galaxyui_core::event::KeyState; use galaxyui_core::integration::{AssertionOutcome, StepData, TestStep}; use galaxyui_core::keymap::{Keystroke, PerPlatformKeystroke, Trigger}; use galaxyui_core::platform::keyboard::KeyCode; use galaxyui_core::platform::{OperatingSystem, TerminationMode}; use galaxyui_core::units::Lines; use galaxyui_core::windowing::WindowManager; use galaxyui_core::{ async_assert, async_assert_eq, AssetProvider, Event, SingletonEntity, UpdateView, ViewHandle, }; pub use goto_line::*; pub use history::*; pub use input::*; pub use keyboard_protocol::*; pub use launch_configs::*; pub use notebooks::*; pub use pane_restoration::*; use parking_lot::Mutex; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; #[cfg(target_os = "macos")] pub use preview_config_migration::*; pub use remote_server::*; pub use rich_input_ctrl_enter::*; pub use rules::*; use rust_embed::RustEmbed; pub use secrets::*; pub use session_restoration::*; use settings::Setting as _; pub use settings_file_errors::*; pub use settings_file_hot_reload::*; pub use settings_file_migration::*; pub use settings_private::*; use shell::ShellType; pub use ssh::*; pub use subshell::*; use sum_tree::SeekBias; pub use sync_inputs::*; use sysinfo::{Pid, ProcessesToUpdate, System}; pub use typeahead::*; use version_compare::Cmp; pub use video_recording::*; use warp::appearance::Appearance; use warp::features::FeatureFlag; use warp::integration_testing::assertions::{ assert_binding_display_string, go_offline, go_online, join_a_workspace, }; use warp::integration_testing::block::{ assert_block_visible, assert_bottom_of_block_approx_at, assert_num_blocks_in_model, BlockPosition, LinePosition, }; use warp::integration_testing::clipboard::assert_clipboard_contains_string; use warp::integration_testing::command_palette::{ close_command_palette, open_command_palette, open_command_palette_and_run_action, TestStepsExt, }; use warp::integration_testing::context_chips::assert_working_dir_is_present; use warp::integration_testing::find::{Find, FindWithinBlockState}; use warp::integration_testing::input::{ input_contains_string, input_is_empty, open_input_context_menu, }; use warp::integration_testing::navigation_palette::{ check_recency, navigate_to_other_session_step, open_navigation_palette_step, RecentSession, }; use warp::integration_testing::pane_group::assert_focused_pane_index; use warp::integration_testing::settings::{ assert_theme_chooser_contains, set_window_custom_size, toggle_setting, }; use warp::integration_testing::step::{ assert_no_pending_model_events, new_step_with_default_assertions, new_step_with_default_assertions_for_pane, }; use warp::integration_testing::tab::{assert_pane_title, assert_tab_title, tab_title_step}; use warp::integration_testing::terminal::util::{ current_shell_starter_and_version, ExactLine, ExpectedExitStatus, }; use warp::integration_testing::terminal::{ assert_active_block_output, assert_active_block_output_for_single_terminal_in_tab, assert_active_block_received_precmd, assert_alt_grid_active, assert_alt_screen_output, assert_command_executed_for_single_terminal_in_tab, assert_context_menu_is_open, assert_focused_editor_in_tab, assert_gap_exists, assert_input_at_bottom_of_terminal, assert_input_at_top_of_terminal, assert_input_mode, assert_input_not_at_either_edge_of_terminal, assert_long_running_block_executing, assert_long_running_block_executing_for_single_terminal_in_tab, assert_model_term_mode, assert_pane_group_has_state, assert_scroll_position, assert_selected_block_index_is_first_renderable, assert_selected_block_index_is_last_renderable, assert_single_terminal_in_tab_bootstrapped, assert_snackbar_is_not_visible, assert_snackbar_is_visible, assert_terminal_bootstrapped, assert_terminal_bootstrapping, assert_view_has_text_selection, assert_waterfall_gap_empty_background_rendered, clear_blocklist_to_remove_bootstrapped_blocks, execute_command_for_single_terminal_in_tab, execute_echo, execute_long_running_command, execute_long_running_command_for_pane, execute_python_interpreter_in_tab, open_context_menu_for_selected_block, performance_test, run_alt_grid_program, run_completer, validate_git_branch, wait_until_bootstrapped_pane, wait_until_bootstrapped_single_pane_for_tab, }; use warp::integration_testing::view_getters::{ pane_group_view, single_input_suggestions_view_for_tab, single_input_view_for_tab, single_terminal_pane_view_for_tab, single_terminal_view, single_terminal_view_for_tab, workspace_view, }; use warp::integration_testing::warp_drive::{ assert_is_left_panel_open, assert_warp_drive_is_closed, assert_warp_drive_is_open, }; use warp::integration_testing::window::{ add_and_save_window, add_window, add_window_and_check_bounds, close_window, save_active_window_id, }; use warp::integration_testing::workspace::assert_tab_count; use warp::integration_testing::{self, view_of_type}; use warp::pane_group::AGENT_MODE_PANE_DEFAULT_MINIMUM_WIDTH; use warp::settings::{ CompletionsOpenWhileTyping, CtrlTabBehavior, MonospaceFontSize, TabBehavior, INPUT_MODE, }; use warp::settings_view::keybindings::KeybindingsView; use warp::settings_view::{FeaturesPageAction, SettingsAction, SettingsSection, SettingsView}; use warp::terminal::alt_screen_reporting::MouseReportingEnabled; use warp::terminal::available_shells::AvailableShells; use warp::terminal::block_list_viewport::{InputMode, ScrollLines, ScrollPosition}; use warp::terminal::find::TerminalFindModel; use warp::terminal::input::{Input, InputSuggestionsMode}; use warp::terminal::keys_settings::KeysSettings; use warp::terminal::local_tty::TerminalManager as LocalTtyTerminalManager; use warp::terminal::model::ansi::{Handler, InitShellValue}; use warp::terminal::model::blocks::{BlockHeightItem, BlockHeightSummary, TotalIndex}; use warp::terminal::model::grid::grid_handler::TermMode; use warp::terminal::model::grid::Dimensions; use warp::terminal::model::terminal_model::BlockIndex; use warp::terminal::session_settings::{HonorPS1, SessionSettings, StartupShellOverride}; use warp::terminal::view::{ BlockVisibilityMode, TerminalAction, TerminalViewState, ALIAS_EXPANSION_BANNER_SEEN_KEY, }; use warp::terminal::{shell, TerminalView}; use warp::util::bindings::CustomAction; use warp::workflows::categories::CategoriesView; use warp::workspace::{ Workspace, WorkspaceAction, NEW_SESSION_MENU_BUTTON_POSITION_ID, NEW_TAB_BUTTON_POSITION_ID, }; use warp::{cmd_or_ctrl_shift, AgentModeEntrypoint}; pub use websockets::*; pub use workflows::*; pub use workspace::*; use crate::builder::cargo_target_tmpdir; use crate::util::{skip_if_powershell_core_2303, ShellRcType}; use crate::{user_defaults, Builder}; const ADD_NEXT_OCCURRENCE_KEYBINDING: &str = "ctrl-g"; #[derive(Clone, Copy, RustEmbed)] #[folder = "tests/data/"] pub struct TestOnlyAssets; pub static TEST_ONLY_ASSETS: TestOnlyAssets = TestOnlyAssets; impl AssetProvider for TestOnlyAssets { fn get(&self, path: &str) -> Result> { ::get(path) .map(|f| f.data) .ok_or_else(|| anyhow!("no asset exists at path {}", path)) } } use super::util::{self, write_all_rc_files_for_test, write_rc_files_for_test}; fn new_builder() -> Builder { Builder::new() } /// Adds a workflow file, containing two workflows, to the mocked out warp /// config directory and verifies that the workflows appear in the workflow menu. pub fn test_add_workflows_to_warp_config() -> Builder { new_builder() .with_setup(move |utils| { utils.set_env("GALAXY_CONFIG_WATCHER_DELAY_MS", Some((10).to_string())); std::fs::create_dir_all(integration_testing::workflow::workflows_dir()) .expect("Should be able to create workflows dir"); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Should have no local workflows").add_named_assertion( "Should have no local workflows", |app, window_id| { let workflows: ViewHandle = view_of_type(app, window_id, 0); workflows.read(app, |workflows, _| { // Note that this can be a synchronous assertion because unlike the next test step, // we don't have concurrency with a GalaxyConfig watcher thread assert_eq!( workflows.local_workflows().count(), 0, "There should not be any local workflows" ); }); AssertionOutcome::Success }, ), ) .with_step( TestStep::new("Write a new file containing two workflows") .with_setup(|_utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "test_workflow.yaml", &integration_testing::workflow::workflows_dir().join("test_workflow.yaml"), ); }) .add_named_assertion( "The two added workflows should be in the view", |app, window_id| { let workflows: ViewHandle = view_of_type(app, window_id, 0); let num_workflows = workflows.read(app, |workflows, _| workflows.local_workflows().count()); async_assert!( num_workflows == 2, "Expected to find two workflows, instead found {}", num_workflows ) }, ), ) } pub fn test_launch_warp_with_theme_in_warp_config() -> Builder { new_builder() .with_setup(move |utils| { utils.set_env("GALAXY_CONFIG_WATCHER_DELAY_MS", Some((10).to_string())); integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "test_theme.yaml", &integration_testing::themes::themes_dir().join("test_theme.yaml"), ); }) .with_step(assert_theme_chooser_contains("Test Theme", 1)) } /// Adds a theme to the mocked out warp config directory and verifies that /// the theme appears in the theme picker. pub fn test_add_theme_to_warp_config() -> Builder { new_builder() .with_setup(move |utils| { utils.set_env("GALAXY_CONFIG_WATCHER_DELAY_MS", Some((10).to_string())); std::fs::create_dir_all(integration_testing::themes::themes_dir()) .expect("Should be able to create themes dir"); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(assert_theme_chooser_contains("Test Theme", 0)) .with_step(TestStep::new("Write a new theme").with_setup(|_utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "test_theme.yaml", &integration_testing::themes::themes_dir().join("test_theme.yaml"), ); })) .with_step(assert_theme_chooser_contains("Test Theme", 1)) .with_step( TestStep::new("Write another new theme").with_setup(|_utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "test_theme.yaml", &integration_testing::themes::themes_dir().join("test_theme_2.yaml"), ); }), ) .with_step(assert_theme_chooser_contains("Test Theme", 2)) .with_step( TestStep::new("Write another new theme with a name in the .yaml").with_setup( |_utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "test_theme_with_name.yaml", &integration_testing::themes::themes_dir().join("test_theme_3.yaml"), ); }, ), ) .with_step(assert_theme_chooser_contains("test_theme", 1)) .with_step(assert_theme_chooser_contains("Test Theme", 2)) } pub fn test_palette_opens_when_theme_chooser_is_open() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_steps( open_command_palette_and_run_action("Open Theme Picker").add_assertion( |app, window_id| { let views = app.views_of_type(window_id).unwrap(); let workspace: &ViewHandle = views.first().unwrap(); workspace.read(app, |view, _| { async_assert!(view.is_theme_chooser_open(), "Theme chooser should be open") }) }, ), ) .with_step(open_command_palette()) } /// Manually executed test that runs a long-line.sh script. Useful for debugging the performance /// of long output commands. Worth combining with cargo-flamegraph when running. pub fn test_with_long_line() -> Builder { let test_name = "long-line.sh"; new_builder() .with_setup(move |utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, test_name, &utils.test_dir().join(test_name), ) }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(performance_test(0, test_name, 3)) } /// Manually executed test that runs a 24-bit-color.sh script. Useful for debugging the performance /// of background in the grid. Worth combining with cargo-flamegraph when running. pub fn test_with_24_bit_color() -> Builder { let test_name = "24-bit-color.sh"; new_builder() .with_setup(move |utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, test_name, &utils.test_dir().join(test_name), ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(performance_test(0, test_name, 1)) } /// Manually executed benchmark to see the memory usage of the block list. /// This workload executes `cd` and `ls` over and over again. It ends with a /// test step that pauses for five minutes, so you have time to check the /// memory usage. It does this by continually failing a test step. pub fn make_1000_blocks_memory_benchmark() -> Builder { let mut builder = new_builder().with_step(wait_until_bootstrapped_single_pane_for_tab(0)); for _ in 0..1000 { builder = builder.with_step(execute_echo(0)); } builder.with_step( TestStep::new("always fail") .add_assertion(|_, _| AssertionOutcome::failure("always fail".to_string())) .set_timeout(Duration::from_secs(300)), ) } pub fn test_completions_with_autocd() -> Builder { new_builder() .set_should_run_test(|| { let (starter, version) = current_shell_starter_and_version(); match starter.shell_type() { ShellType::Zsh | ShellType::Fish => true, // autocd was not added until Bash 4.0--the `shopt -s autocd` line below will fail // in all versions of Bash before 4.0. ShellType::Bash => { version_compare::compare_to(version, "4", Cmp::Ge).unwrap_or(false) } // TODO(PLAT-751) ShellType::PowerShell => false, } }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test(&dir, "setopt autocd", [ShellRcType::Zsh]); write_rc_files_for_test(&dir, "shopt -s autocd", [ShellRcType::Bash]); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, // cd into the out_dir to avoid relying on anything within the filesystem. concat!("cd ", env!("OUT_DIR")).into(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Enter 't' and hit tab") .with_typed_characters(&["t"]) .with_keystrokes(&["tab"]) .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert autocd completions", move |app, window_id| { let input_suggestions = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions.read(app, |view, _ctx| { async_assert!( view.items().iter().any(|item| item.text() == "tmp/"), "tmp/ should be suggested" ) }) }), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) } pub fn test_single_command() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_echo(0)) } pub fn test_open_and_close_settings() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Open settings tab") .with_keystrokes(&["cmdorctrl-,"]) .add_assertion(assert_tab_count(2)) .add_assertion(assert_tab_title(1, "Settings")) .add_assertion(assert_pane_title(1, 0, "Settings")) .add_assertion(move |app, window_id| { let settings_views: Vec> = app .views_of_type(window_id) .expect("Settings view must exist"); assert_eq!(settings_views.len(), 1); let settings_view = settings_views.first().expect("Settings view must exist"); settings_view.read(app, |view, _| { async_assert_eq!( view.current_settings_section(), SettingsSection::default() ) }) }), ) .with_step( new_step_with_default_assertions("Close the settings tab with close tab button") .with_hover_over_saved_position("close_tab_button:1") .with_click_on_saved_position("close_tab_button:1") .add_assertion(assert_tab_count(1)) .add_assertion(assert_tab_title(0, "~")), ) } pub fn test_open_and_close_theme_creator_modal() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_steps( open_command_palette_and_run_action("Open Theme Picker").add_assertion( |app, window_id| { let views = app.views_of_type(window_id).unwrap(); let workspace: &ViewHandle = views.first().unwrap(); workspace.read(app, |view, _| { async_assert!(view.is_theme_chooser_open(), "Theme chooser should be open") }) }, ), ) .with_step( new_step_with_default_assertions("Click on button to open theme creator modal") .with_click_on_saved_position("create_theme_button") .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let workspace = views.first().unwrap(); workspace.read(app, |view, _| { async_assert!( view.is_theme_creator_modal_open(), "Theme creator modal should be open" ) }) }), ) .with_step( new_step_with_default_assertions("Click on cancel button to close theme creator modal") .with_click_on_saved_position("theme_creator_cancel_button") .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let workspace = views.first().unwrap(); workspace.read(app, |view, _| { async_assert!( !view.is_theme_creator_modal_open(), "Theme creator modal should be closed" ) }) }), ) } pub fn test_suggestions_menu_positioning() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Make sure there are multiple items in the working directory, // otherwise the tab completion menu will not appear (as there is // nothing for the user to choose between). .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir some_dir && touch some_file".to_owned(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Open suggestions") .with_typed_characters(&["ls", " "]) .with_keystrokes(&["tab"]) .add_named_assertion_with_data_from_prior_step( "Assert buffer text and save suggestion position", |app, window_id, step_data| { let input_view = single_input_view_for_tab(app, window_id, 0); let outcome = input_view.read(app, |view, ctx| { view.buffer_text(ctx); async_assert!( view.buffer_text(ctx) == *"ls ", "Input box should contain 'ls '" ) }); app.update(|ctx| { let presenter = ctx.presenter(window_id).expect("window should exist"); let suggestions_menu_x = presenter .borrow() .position_cache() .get_position("input_suggestions:index_0") .unwrap() .origin_x(); step_data.insert("suggestions_menu_x", suggestions_menu_x); }); outcome }, ), ) .with_step( new_step_with_default_assertions("Open Warp Drive") .with_click_on_saved_position("workspace:toggle_left_panel") .add_assertion(assert_is_left_panel_open()), ) .with_step( new_step_with_default_assertions("Assert that suggestions menu updated") .add_named_assertion_with_data_from_prior_step( "Assert suggestions menu shifted to the right", |app, window_id, step_data| { app.update(|ctx| { let presenter = ctx.presenter(window_id).expect("window should exist"); let suggestions_menu_x = presenter .borrow() .position_cache() .get_position("input_suggestions:index_0") .unwrap() .origin_x(); assert!( suggestions_menu_x > *step_data .get("suggestions_menu_x") .expect("data should have been set in earlier step"), "Suggestions menu should have adjusted rightward" ); }); AssertionOutcome::Success }, ), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_click_on_prompt_to_focus_input() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(execute_command_for_single_terminal_in_tab( 0, String::new(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Click on block to unfocus the input box") .with_click_on_saved_position("block_index:0") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( !input.editor().is_focused(ctx), "Input box should not be focused" ) }) }) }), ) .with_step( new_step_with_default_assertions("Click on prompt and verify input box is focused") .with_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("prompt_area_{}", input.id()) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box should be focused" ) }) }) }), ) } pub fn test_clear() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); // A gap should exist in the model. let num_gaps = model .block_list() .block_heights() .cursor::() .filter(|item| matches!(item, BlockHeightItem::Gap { .. })) .count(); async_assert_eq!( 1, num_gaps, "Block list should have one gap but it has {}", num_gaps ) }) }), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Select last block and verify it's selected") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_step( new_step_with_default_assertions("Hit up arrow to navigate up") .with_keystrokes(&["up"]) .add_assertion(assert_selected_block_index_is_first_renderable()), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions( "Run clear again and ensure there is still only one block", ) .with_keystrokes(&["ctrl-l"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let terminal_view: &ViewHandle = views.first().unwrap(); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); // A single gap should exist in the model. let num_gaps = model .block_list() .block_heights() .cursor::() .filter(|item| matches!(item, BlockHeightItem::Gap { .. })) .count(); async_assert_eq!( 1, num_gaps, "Block list should have one gap but it has {}", num_gaps ) }) }), ) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "clear".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Clear viewport using `clear` command leaves one gap") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); // A gap should exist in the model. let num_gaps = model .block_list() .block_heights() .cursor::() .filter(|item| matches!(item, BlockHeightItem::Gap { .. })) .count(); async_assert_eq!( 1, num_gaps, "Block list should have one gap but it has {}", num_gaps ) }) }), ) .with_step( new_step_with_default_assertions("`clear` block should not be visible").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { // Similar logic to `is_block_visible` in terminal/view.rs let model = view.model.lock(); let block_list = model.block_list(); let last_block = match block_list.last_non_hidden_block() { None => { return AssertionOutcome::failure( "No non-hidden block found".to_string(), ); } Some(last_block) => last_block, }; let visible_rows = view.content_element_height_lines(ctx); let top_offset = view .viewport_state(model.block_list(), InputMode::PinnedToBottom, ctx) .scroll_top_in_lines(); let mut block_heights_cursor = block_list .block_heights() .cursor::(); block_heights_cursor.seek(&last_block.index(), SeekBias::Right); // BlockVisibilityMode::CommandAndPromptVisible let command_bottom_offset = block_heights_cursor.start().height; let block_is_visible = top_offset < command_bottom_offset && (top_offset + visible_rows) > command_bottom_offset; async_assert!( !block_is_visible, "Expected `clear` command block to not be visible" ) }) }, ), ) .with_step( new_step_with_default_assertions( "Select last block (`clear`) and verify it's selected", ) .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_waterfall_input_alt_grid() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([( INPUT_MODE.to_owned(), serde_json::to_string(&InputMode::Waterfall) .expect("input_mode value should convert to json string"), )])) .with_step( wait_until_bootstrapped_single_pane_for_tab(0) .add_assertion(assert_input_mode(InputMode::Waterfall)), ) .with_step( clear_blocklist_to_remove_bootstrapped_blocks() .add_assertion(assert_waterfall_gap_empty_background_rendered(true)), ); let steps = run_alt_grid_program( "vim", 0, 0, TestStep::new("Close vim") .with_typed_characters(&[":q"]) .with_keystrokes(&["enter"]), vec![TestStep::new("waterfall background should not be rendered") .add_assertion(assert_waterfall_gap_empty_background_rendered(false))], ); builder = builder.with_steps(steps); // Add one more step for letting the block list get back to a normal state builder = builder.with_step(new_step_with_default_assertions("return to block list")); builder } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_waterfall_input() -> Builder { new_builder() .with_user_defaults(HashMap::from([ ( INPUT_MODE.to_owned(), serde_json::to_string(&InputMode::Waterfall) .expect("input_mode value should convert to json string"), ), // Ensure the alias expansion doesn't appear (the use of "echo" could cause it to appear // in Powershell since `echo` is an alias for `Write-Output`). ( ALIAS_EXPANSION_BANNER_SEEN_KEY.to_owned(), serde_json::to_string(&true).expect("bool should convert to JSON string"), ), ])) .with_step( wait_until_bootstrapped_single_pane_for_tab(0) .add_assertion(assert_input_mode(InputMode::Waterfall)), ) .with_step( clear_blocklist_to_remove_bootstrapped_blocks() .add_assertion(assert_num_blocks_in_model(1)), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()) .add_assertion(assert_block_visible( BlockPosition::LastBlock, BlockVisibilityMode::TopOfBlockVisible, false, )) .add_assertion(assert_bottom_of_block_approx_at( BlockPosition::LastBlock, LinePosition::AtScrollTop, )), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Select last block and verify it's selected") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()) .add_assertion(assert_input_not_at_either_edge_of_terminal()), ) .with_step( new_step_with_default_assertions("Hit up arrow to navigate up") .with_keystrokes(&["up"]) .add_assertion(assert_selected_block_index_is_first_renderable()) .add_assertion(assert_input_not_at_either_edge_of_terminal()), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions( "Run clear again and ensure there is still only one block", ) .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()) .add_assertion(assert_block_visible( BlockPosition::LastBlock, BlockVisibilityMode::TopOfBlockVisible, false, )) .add_assertion(assert_bottom_of_block_approx_at( BlockPosition::LastBlock, LinePosition::AtScrollTop, )), ) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "clear".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Clear viewport using `clear` command leaves one gap") .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()), ) .with_step( new_step_with_default_assertions("`clear` block should not be visible").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { // Similar logic to `is_block_visible` in terminal/view.rs let model = view.model.lock(); let block_list = model.block_list(); let last_block = match block_list.last_non_hidden_block() { None => { return AssertionOutcome::failure( "No non-hidden block found".to_string(), ); } Some(last_block) => last_block, }; let visible_rows = view.content_element_height_lines(ctx); let top_offset = view .viewport_state(model.block_list(), InputMode::Waterfall, ctx) .scroll_top_in_lines(); let mut block_heights_cursor = block_list .block_heights() .cursor::(); block_heights_cursor.seek(&last_block.index(), SeekBias::Right); let command_bottom_offset = block_heights_cursor.start().height; let block_is_visible = top_offset < command_bottom_offset && (top_offset + visible_rows) > command_bottom_offset; async_assert!( !block_is_visible, "Expected `clear` command block to not be visible" ) }) }, ), ) .with_step( new_step_with_default_assertions( "Select last block (`clear`) and verify it's selected", ) .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) } pub fn test_waterfall_input_text_selection() -> Builder { new_builder() .with_user_defaults(HashMap::from([( INPUT_MODE.to_owned(), serde_json::to_string(&InputMode::Waterfall) .expect("input_mode value should convert to json string"), )])) .with_step( wait_until_bootstrapped_single_pane_for_tab(0) .add_assertion(assert_input_mode(InputMode::Waterfall)), ) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()), ) // Run three commands after the clear .with_step(execute_echo(0)) .with_step(execute_echo(0)) .with_step(execute_echo(0).add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { async_assert!(!view.is_selecting(), "Should not be selecting",) }) })) .with_step( // Drag from the top left to the bottom right. The exact numbers are overshoots here but // are big enough to capture all of the blocks. new_step_with_default_assertions("start selecting") .with_event(Event::LeftMouseDown { position: Vector2F::new(415., 50.), modifiers: Default::default(), click_count: 1, is_first_mouse: false, }) .with_event(Event::MouseMoved { position: Vector2F::new(400., 300.), cmd: false, shift: false, is_synthetic: false, }) .add_assertion(assert_view_has_text_selection(true)), ) .with_step( new_step_with_default_assertions("end selecting") .with_event(Event::LeftMouseUp { position: Vector2F::new(400., 300.), modifiers: Default::default(), }) .add_assertion(assert_view_has_text_selection(false)), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_waterfall_input_scrolling() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([ ( INPUT_MODE.to_owned(), serde_json::to_string(&InputMode::Waterfall) .expect("input_mode value should convert to json string"), ), // Ensure the alias expansion doesn't appear (the use of "echo" could cause it to appear // in Powershell since `echo` is an alias for `Write-Output`). ( ALIAS_EXPANSION_BANNER_SEEN_KEY.to_owned(), serde_json::to_string(&true).expect("bool should convert to JSON string"), ), ])) .with_step( wait_until_bootstrapped_single_pane_for_tab(0) .add_assertion(assert_input_mode(InputMode::Waterfall)), ) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()), ) // Fill up the blocklist with a single long block. .with_step(create_long_block()) // The gap should be gone .with_step(execute_echo(0).add_assertion(assert_gap_exists(false))); fn clear_and_arrow_up_to_top(builder: Builder) -> Builder { builder .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()), ) .with_step( new_step_with_default_assertions("Hit up arrow to navigate up") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_input_not_at_either_edge_of_terminal()), ) // Select to the top block - should force scrolling up .with_step( new_step_with_default_assertions("Navigate to top") .with_keystrokes(&["up"; 20]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scrolled_up = matches!( view.scroll_position(), ScrollPosition::FixedAtPosition { .. } ); async_assert!(scrolled_up, "Should be scrolled up",) }) }), ) } builder = clear_and_arrow_up_to_top(builder); // Make sure executing a small command keeps the input at the bottom. builder = builder .with_step( new_step_with_default_assertions("Focus the input box") .with_keystrokes(&[cmd_or_ctrl_shift("l")]), ) .with_step(execute_echo(0).add_assertion(assert_input_at_bottom_of_terminal())); // Arrow up should scroll the view up. builder = clear_and_arrow_up_to_top(builder); // Make sure executing a large command that clears the gap keeps the input at the bottom. builder = builder .with_step( new_step_with_default_assertions("Focus the input box before long command") .with_keystrokes(&[cmd_or_ctrl_shift("l")]), ) .with_step( create_long_block() .add_assertion(assert_gap_exists(false)) .add_assertion(assert_input_at_bottom_of_terminal()), ); builder } pub fn test_waterfall_input_after_command_execution() -> Builder { new_builder() .with_user_defaults(HashMap::from([( INPUT_MODE.to_owned(), serde_json::to_string(&InputMode::Waterfall) .expect("input_mode value should convert to json string"), )])) .with_step( wait_until_bootstrapped_single_pane_for_tab(0) .add_assertion(assert_input_mode(InputMode::Waterfall)), ) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) // Fill up the blocklist with a single long block. .with_step(create_long_block()) // The gap should be gone .with_step(execute_echo(0).add_assertion(assert_gap_exists(false))) .with_step( new_step_with_default_assertions("Clear viewport using ctrl-l") .with_keystrokes(&["ctrl-l"]) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_top_of_terminal()) .add_assertion(assert_block_visible( BlockPosition::LastBlock, BlockVisibilityMode::TopOfBlockVisible, false, )) .add_assertion(assert_bottom_of_block_approx_at( BlockPosition::LastBlock, LinePosition::AtScrollTop, )), ) // Now make sure that when we execute a command the scroll position is preserved .with_step( execute_echo(0) // The gap should still exist .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_not_at_either_edge_of_terminal()) .add_assertion(assert_block_visible( BlockPosition::LastBlock, BlockVisibilityMode::TopOfBlockVisible, true, )) .add_assertion(assert_bottom_of_block_approx_at( BlockPosition::LastBlock, LinePosition::AtTopOfInput, )) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let waterfall_gap_scrolled = matches!( view.scroll_position(), ScrollPosition::WaterfallGapFollowsBottomOfMostRecentBlock { .. } ); async_assert!(waterfall_gap_scrolled, "Should have gap scrolling") }) }), ) // Now try a long running command and make sure that the input goes away .with_step( TestStep::new("Execute sleep") .with_typed_characters(&["sleep 999"]) .with_keystrokes(&["enter"]) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(true, 0), ) .add_assertion(assert_gap_exists(true)) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let waterfall_gap_scrolled = matches!( view.scroll_position(), ScrollPosition::WaterfallGapFollowsBottomOfMostRecentBlock { .. } ); async_assert!(waterfall_gap_scrolled, "Should have gap scrolling") }) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { async_assert_eq!( view.input_size_at_last_frame(ctx) .expect("Input should have been laid out") .y(), 0., "Input should have zero height" ) }) }), ) .with_step( new_step_with_default_assertions("Check ctrl-c terminates the command") .with_keystrokes(&["ctrl-c"]) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); async_assert!( !model .block_list() .active_block() .is_active_and_long_running(), "Check if the command has terminated" ) }) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { async_assert!( view.input_size_at_last_frame(ctx).unwrap_or_default().y() > 0., "Input should have non-zero height" ) }) }), ) .with_step( new_step_with_default_assertions("Hit up arrow to navigate up") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_input_not_at_either_edge_of_terminal()), ) // Select to the top block - should force scrolling up .with_step(new_step_with_default_assertions("Navigate to top").with_keystrokes(&["up"; 30])) .with_step( new_step_with_default_assertions("Check scroll position at top / input at bottom") .with_keystrokes(&["up"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let ScrollPosition::FixedAtPosition { scroll_lines: ScrollLines::ScrollTop(scroll_top_in_lines), } = view.scroll_position() else { return AssertionOutcome::failure( "Should be fixed at position".to_string(), ); }; async_assert_eq!( Lines::zero(), scroll_top_in_lines, "Should be scrolled to_top" ) }) }) .add_assertion(assert_input_at_bottom_of_terminal()), ) // Execute a command and make sure the bottom of it is in the right place .with_step( execute_echo(0) .add_assertion(assert_gap_exists(true)) .add_assertion(assert_input_at_bottom_of_terminal()), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_text_input_on_block_list() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( clear_blocklist_to_remove_bootstrapped_blocks() ) .with_step(execute_command_for_single_terminal_in_tab(0, String::new(), ExpectedExitStatus::Success, ())) .with_step( new_step_with_default_assertions("Click on block to unfocus the input box") .with_click_on_saved_position("block_index:0") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( !input.editor().is_focused(ctx), "Input box should not be focused" ) }) }) }), ) .with_step( new_step_with_default_assertions( "Mock keydown on 'a' and ensure input box is focused with the character a entered", ) .with_keystrokes(&["a"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let terminal_view: &ViewHandle = views.first().unwrap(); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { assert!( input.editor().is_focused(ctx), "Input box should be focused." ); async_assert!( input.buffer_text(ctx) == *"a", "Input box buffer had had {}", input.buffer_text(ctx) ) }) }) }), ) .with_step( new_step_with_default_assertions("Click on block to focus the terminal view") .with_click_on_saved_position("block_index:0") .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( new_step_with_default_assertions( "Regression test step to ensure control characters (e.g. escape) don't appear in input", ) .with_event(Event::KeyDown { keystroke: Keystroke { key: "escape".to_string(), ..Default::default() }, chars: "\u{1b}".to_string(), details: Default::default(), is_composing: false, }) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).expect("Should be able to retrieve view"); let terminal_view: &ViewHandle = views.first().expect("Should be a terminal view"); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { assert!( input.editor().is_focused(ctx), "Input box should be focused." ); async_assert!( input.buffer_text(ctx) == *"a", "Input box buffer should be unchanged but had {}", input.buffer_text(ctx) ) }) }) }), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_text_input_on_block_list_while_composing() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( clear_blocklist_to_remove_bootstrapped_blocks() ) .with_step(execute_command_for_single_terminal_in_tab(0, String::new(), ExpectedExitStatus::Success, ())) .with_step( new_step_with_default_assertions("Click on block to unfocus the input box") .with_click_on_saved_position("block_index:0") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( !input.editor().is_focused(ctx), "Input box should not be focused after clicking on block." ) }) }) }), ) .with_step( new_step_with_default_assertions("Mock keydown on 'option-e' + 'a' in composing state and ensure input box is not focused") .with_keystrokes_in_composing(&["alt-e", "a"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( !input.editor().is_focused(ctx), "Input box should not be focused after option-e." ) }) }) }), ) .with_step( new_step_with_default_assertions( "Mock typedcharacters on 'á' and ensure input box is focused and its buffer is updated", ) .with_typed_characters(&["á"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let terminal_view: &ViewHandle = views.first().unwrap(); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.buffer_text(ctx) == *"á", "Input box buffer should be updated." ) }) }) }), ) } pub fn test_unnecessary_resizes() -> Builder { struct SizeData { rows: usize, cols: usize, } let size_data = Rc::new(Mutex::new(SizeData { rows: 0, cols: 0 })); let size_data_store = size_data.clone(); let size_data_lines = size_data.clone(); new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Collect size information") .add_assertion( move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let terminal_view = views.first().unwrap(); terminal_view.read(app, |view, _| { let model = view.model.lock(); let mut size_data = size_data_store.lock(); let alt_screen_grid = model.alt_screen().grid_handler(); size_data.rows = alt_screen_grid.visible_rows(); size_data.cols = alt_screen_grid.columns(); let active_block_grid = model.block_list().active_block().output_grid().grid_handler(); assert!( active_block_grid.visible_rows() == size_data.rows && active_block_grid.columns() == size_data.cols, "Block and alt screen grids should have the same size" ); AssertionOutcome::Success }) }, ), ) .with_step( new_step_with_default_assertions("Editor expanding to multiple lines doesn't cause resize") .with_keystrokes(&[ "shift-enter", "shift-enter", ]) .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let terminal_view = views.first().unwrap(); terminal_view.read(app, |view, _| { let model = view.model.lock(); let size_data = size_data_lines.lock(); let alt_screen_grid = model.alt_screen().grid_handler(); let active_block_grid = model.block_list().active_block().output_grid().grid_handler(); assert!( alt_screen_grid.visible_rows() == size_data.rows && alt_screen_grid.columns() == size_data.cols && active_block_grid.visible_rows() == size_data.rows && active_block_grid.columns() == size_data.cols, "Grids should not be resized when the editor expands" ); AssertionOutcome::Success }) }), ) .with_step( TestStep::new("Long-running command doesn't cause resize") .with_typed_characters(&["python3"]) .with_keystrokes(&["enter"]) .add_named_assertion("no pending model events", assert_no_pending_model_events()) .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let terminal_view = views.first().unwrap(); terminal_view.read(app, |view, _| { let model = view.model.lock(); let size_data = size_data.lock(); let alt_screen_grid = model.alt_screen().grid_handler(); let active_block = model.block_list().active_block(); let active_block_grid = active_block.output_grid().grid_handler(); async_assert!( active_block.is_active_and_long_running() && alt_screen_grid.visible_rows() == size_data.rows && alt_screen_grid.columns() == size_data.cols && active_block_grid.visible_rows() == size_data.rows && active_block_grid.columns() == size_data.cols, "Grids should not be resized when the editor is hidden for long-running commands. is_active_and_long_running {}", active_block.is_active_and_long_running() ) }) }), ) } pub fn test_undo_redo() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Input command") .with_typed_characters(&["foo"]) .set_timeout(Duration::from_secs(5)) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { view.buffer_text(ctx); async_assert!( view.buffer_text(ctx) == *"foo", "Input box should have the typed characters" ) }) }), ) .with_step( new_step_with_default_assertions("Undo") .with_keystrokes(&["cmdorctrl-z"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!(view.buffer_text(ctx) == *"", "Input box should be empty") }) }), ) .with_step( new_step_with_default_assertions("Redo") .with_per_platform_keystroke(PerPlatformKeystroke { mac: "shift-cmd-Z", linux_and_windows: "shift-ctrl-Z", }) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( view.buffer_text(ctx) == *"foo", "Input box should have the typed characters after redo" ) }) }), ) .with_step( new_step_with_default_assertions("Execute command to clear input box") .with_keystrokes(&["enter"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!(view.buffer_text(ctx) == *"", "Input box should be empty") }) }), ) .with_step( new_step_with_default_assertions("Undo should be no-op as undo stack should be reset") .with_keystrokes(&["cmdorctrl-z"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( view.buffer_text(ctx) == *"", "Input box should remain empty because undo stack is empty" ) }) }), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_open_context_menu_and_execute_command() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step( new_step_with_default_assertions("Run ls and verify block exists") .with_keystrokes(&["l", "s", "enter"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); async_assert_eq!( 2, model.block_list().blocks().len(), "Block list should have two blocks but it has {}", model.block_list().blocks().len() ) }) }), ) .with_step( new_step_with_default_assertions("Hover over the recently created block") .with_hover_over_saved_position("block_index:0") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { assert_eq!( Some(BlockIndex::zero()), view.hovered_block_index(), "Expected first block to be hovered over, but got block index {:?}", view.hovered_block_index() ); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions("Click on context menu button") .with_click_on_saved_position("context_menu_button_0") .add_assertion(assert_context_menu_is_open(true)), ) .with_step( new_step_with_default_assertions("Select context menu action") .with_click_on_saved_position("Copy command") .add_assertion(assert_clipboard_contains_string("ls".into())), ) } pub fn test_open_and_close_context_menu_with_keybinding() -> Builder { new_builder() // The ctrl-m keybinding to open a block context menu is only set on Mac. So that we can // test this behavior on all platforms, create a fake custom keybindings file that forces // this action to have a binding of `ctrl-m`. .with_setup(|_utils| { integration_testing::create_file_with_contents( r#""terminal:open_block_list_context_menu_via_keybinding": ctrl-m"#.as_bytes(), &integration_testing::keybindings::keybinding_file_path(), ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls -a".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Select recently created block with cmd-up") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_step( new_step_with_default_assertions("Press keybinding to open context menu") .with_keystrokes(&["ctrl-m"]) .add_assertion(assert_context_menu_is_open(true)), ) .with_step( new_step_with_default_assertions("Press keybinding again to close context menu") .with_keystrokes(&["ctrl-m"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let workspace: &ViewHandle = views.first().unwrap(); let is_overflow_menu_showing = workspace.read(app, |workspace, _| workspace.is_overflow_menu_showing()); async_assert!( !is_overflow_menu_showing, "Expected overflow menu not to be showing", ) }), ) } pub fn test_open_input_context_menu() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Right click to open input context menu") .with_typed_characters(&["pwd"]) .with_action(move |app, _, _| { let window_id = app.read(|ctx| ctx.windows().active_window().expect("no active window")); let terminal_view_id = single_terminal_view(app, window_id).id(); app.dispatch_typed_action( window_id, &[terminal_view_id], &TerminalAction::OpenInputContextMenu { position: Vector2F::new(8.5, 8.5), }, ); }) .add_assertion(assert_context_menu_is_open(true)), ) } pub fn test_copy_all_from_input_context_menu() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Type something into input") .with_typed_characters(&["all of this should be copied"]), ) .with_step(open_input_context_menu()) .with_step( new_step_with_default_assertions("Press Select all in context menu") .with_keystrokes(&["down", "enter"]), ) .with_step(open_input_context_menu()) .with_step( new_step_with_default_assertions("Press Copy in context menu") .with_keystrokes(&["down", "down", "enter"]) .add_assertion(assert_clipboard_contains_string( "all of this should be copied".into(), )), ) } pub fn test_cut_paste_from_input_context_menu() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Type something into input") .with_typed_characters(&["this should be cut then pasted"]) .add_assertion(input_contains_string( 0, "this should be cut then pasted".to_owned(), )), ) .with_step( new_step_with_default_assertions("Select all text then press Cut in context menu") .with_keystrokes(&[cmd_or_ctrl_shift("a")]), ) .with_step(open_input_context_menu()) .with_step( new_step_with_default_assertions("Cut text using context menu") .with_keystrokes(&["down", "enter"]) // Cut is the first menu item when text is selected .add_assertion(assert_clipboard_contains_string( "this should be cut then pasted".into(), )) .add_assertion(input_is_empty(0)), ) .with_step(open_input_context_menu()) .with_step( new_step_with_default_assertions("Press Paste in context menu") .with_keystrokes(&["down", "enter"]) // Paste is the first menu item when input editor is empty .add_assertion(input_contains_string( 0, "this should be cut then pasted".to_owned(), )), ) } pub fn test_block_metadata_received() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("First block in blocklist has metadata") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { async_assert_eq!( view.model .lock() .block_list() .blocks() .last() .expect("block list cannot be empty") .shell_host() .expect("shell_host must be set") .shell_type, view.active_session_shell_type(ctx) .expect("terminal must have an active shell"), "block shell should be correct", ) }) }), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_scroll_to_hidden_block_and_open_context_menu_with_keybinding() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(create_long_block()) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Shift the focus to block list") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { let is_input_focused = view.input().as_ref(ctx).editor().as_ref(ctx).is_focused(); let scroll_position = view.scroll_position(); async_assert!(!is_input_focused && matches!(scroll_position, ScrollPosition::FollowsBottomOfMostRecentBlock), "input should not be focused and view shouldn't scroll up") }) }) ) .with_step( new_step_with_default_assertions("Select first block without scrolling") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); app.update_view(&terminal_view, |view, ctx| { view.integration_test_change_block_selection_to_single(BlockIndex::zero(), ctx) }); terminal_view.read(app, |view, _ctx| { async_assert_eq!( view.selected_blocks_tail_index().unwrap(), BlockIndex::zero(), "first block should be selected" ) }) }) ) .with_steps({ let mut steps = open_context_menu_for_selected_block(); let last = steps.pop().expect("steps should not be empty"); steps.push(last.add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scroll_position = view.scroll_position(); async_assert!( matches!(scroll_position, ScrollPosition::FixedAtPosition{..}) && view.is_context_menu_open(), "Expected to scroll up from keybinding press and the context menu should be open" ) }) })); steps }) } pub fn test_home_key_should_not_appear_in_input() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions( "Press home key and verify it doesn't show up in input", ) .with_keystrokes(&["home"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let input_view: &ViewHandle = views.first().unwrap(); input_view.read(app, |view, ctx| { assert_eq!( view.buffer_text(ctx), String::new(), "Input should be empty" ); AssertionOutcome::Success }) }), ) } pub fn test_change_font_size() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Press alt-shift-> and verify font size increases") .with_keystrokes(&["alt-shift->"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, _ctx| { view.editor().read(app, |editor, ctx| { let appearance = Appearance::as_ref(ctx); // Since user defaults are empty to start in an integration test, // we expect that increasing the font size will change // the editor font size to one more than the default. async_assert!( ((editor.font_size(appearance) - 1.) - MonospaceFontSize::default_value()) .abs() < f32::EPSILON, "Font size should be greater than default" ) }) }) }), ) .with_step( new_step_with_default_assertions("Press alt-shift-< and verify font size decreases") .with_keystrokes(&["alt-shift-<"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, _ctx| { view.editor().read(app, |editor, ctx| { let appearance = Appearance::as_ref(ctx); async_assert!( (editor.font_size(appearance) - MonospaceFontSize::default_value()) .abs() < f32::EPSILON, "Font size should be back to the original font size" ) }) }) }), ) } pub fn test_removing_tabs_out_of_order() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Add a second tab with new tab button") .with_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID) .add_assertion(assert_tab_count(2)), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) .with_step( new_step_with_default_assertions("Add a third tab with the new tab button") .with_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID) .add_assertion(assert_tab_count(3)), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(2)) .with_step( new_step_with_default_assertions("Switch to the first tab") .with_keystrokes(&["cmdorctrl-1"]) .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).unwrap(); let workspace: &ViewHandle = views.first().unwrap(); let (active_tab_id, first_tab_id) = workspace.read(app, |workspace, _| { ( workspace.active_tab_pane_group().id(), workspace.get_pane_group_view_unchecked(0).id(), ) }); async_assert_eq!( active_tab_id, first_tab_id, "Expected first tab (ID {}) to be active, but was (ID {})", first_tab_id, active_tab_id, ) }), ) .with_step( new_step_with_default_assertions("Close the first tab") .with_keystrokes(&[cmd_or_ctrl_shift("w")]) .add_assertion(assert_tab_count(2)), ) .with_step( new_step_with_default_assertions("Close the new first tab (which was the 2nd tab)") .with_keystrokes(&[cmd_or_ctrl_shift("w")]) .add_assertion(assert_tab_count(1)), ) } pub fn test_add_and_close_session() -> Builder { let pid = Rc::new(std::cell::RefCell::new(0_u32)); let pid_clone = pid.clone(); new_builder() .with_user_defaults(HashMap::from([( "UndoCloseEnabled".to_string(), false.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions( "Add a second session using cmd-t and verify it bootstraps", ) .with_keystrokes(&[cmd_or_ctrl_shift("t")]) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 1) }) .add_assertion(assert_tab_count(2)), ) .with_step( new_step_with_default_assertions("Check if session is created properly") .set_timeout(Duration::from_secs(10)) .add_assertion(move |app, window_id| { let views = app.views_of_type(window_id).unwrap(); let workspace: &ViewHandle = views.first().unwrap(); let shell_pid = workspace.read(app, |workspace, _| { workspace .get_pane_group_view_unchecked(1) .read(app, |pane_group, ctx| { pane_group .terminal_manager(0, ctx) .expect("pane at index 0 is a terminal pane") .as_ref(ctx) .as_any() .downcast_ref::>() .expect("terminal pane at index 0 contains a local session") .pid() .expect("shell should be spawned") }) }); *pid.borrow_mut() = shell_pid; let mut system = System::new(); system.refresh_processes( ProcessesToUpdate::Some(&[Pid::from_u32(shell_pid)]), false, /* remove_dead_processes */ ); let process = system.process(Pid::from_u32(shell_pid)); async_assert!( shell_pid != 0 && process.is_some(), "The pid should be active" ) }), ) .with_step( new_step_with_default_assertions("Remove second session") .with_keystrokes(&[cmd_or_ctrl_shift("w")]) .add_assertion(assert_tab_count(1)), ) .with_step( new_step_with_default_assertions("Check if session is terminated properly") .add_assertion(move |_, _| { let mut system = System::new(); let shell_pid = *pid_clone.borrow(); system.refresh_processes( ProcessesToUpdate::Some(&[Pid::from_u32(shell_pid)]), false, /* remove_dead_processes */ ); let process = system.process(Pid::from_u32(shell_pid)); async_assert!( process.is_none(), "The pid {} should have been terminated", shell_pid ) }), ) .with_step( new_step_with_default_assertions("Add a tab with new session button") .with_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID) .add_assertion(assert_tab_count(2)), ) .with_step( new_step_with_default_assertions("Close the first tab with close tab button") .with_hover_over_saved_position("close_tab_button:0") .with_click_on_saved_position("close_tab_button:0") .add_assertion(assert_tab_count(1)), ) } pub fn test_open_and_close_resource_center() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Click the resource center button to show the menu") .with_hover_over_saved_position("resource_center_button") .with_click_on_saved_position("resource_center_button") .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).expect("No workspace found"); let workspace: &ViewHandle = views.first().expect("No workspace in views"); let is_resource_center_showing = workspace.read(app, |workspace, _| workspace.is_resource_center_showing()); async_assert!( is_resource_center_showing, "Expected resource center to be showing", ) }), ) .with_step( new_step_with_default_assertions("Click the resource center button to hide the menu") .with_hover_over_saved_position("resource_center_button") .with_click_on_saved_position("resource_center_button") .add_assertion(|app, window_id| { let views = app.views_of_type(window_id).expect("No workspace found"); let workspace: &ViewHandle = views.first().expect("No workspace in views"); let is_resource_center_showing = workspace.read(app, |workspace, _| workspace.is_resource_center_showing()); async_assert!( !is_resource_center_showing, "Expected resource center not to be showing", ) }), ) } pub fn test_add_many_sessions() -> Builder { let mut builder = new_builder().with_step(wait_until_bootstrapped_single_pane_for_tab(0)); for i in 1..5 { let tab_idx = i; builder = builder .with_step( new_step_with_default_assertions( format!("Add a session {i} using cmd-t and verify it bootstraps").as_str(), ) .with_keystrokes(&[cmd_or_ctrl_shift("t")]) .set_timeout(Duration::from_secs(10)) .add_assertion(move |app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, tab_idx) }) .add_assertion(assert_tab_count(tab_idx + 1)), ) .with_step(execute_echo(tab_idx)) } builder } pub fn test_ctrl_tab_session_switching() -> Builder { #[allow(unused_mut, unused_assignments)] let mut builder = new_builder(); // If linux return early. For reasons unknown and not worth the time to debug currently // this test fails on linux at the step where the command palette is expected to show. // The feature does work on linux though - there's some underlying issue with our integration // test here. #[cfg(any(target_os = "linux", target_os = "freebsd"))] { return builder; } // if we are on linux, allow unreachable code #[allow(unreachable_code)] { builder = new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_echo(0)) .with_step( toggle_setting(SettingsAction::FeaturesPageToggle( FeaturesPageAction::SetCtrlTabBehavior(CtrlTabBehavior::CycleMostRecentSession), )) .add_assertion(|app, _| { let ctrl_tab_behavior = KeysSettings::handle(app) .read(app, |keys_settings, _| *keys_settings.ctrl_tab_behavior); async_assert!( matches!(ctrl_tab_behavior, CtrlTabBehavior::CycleMostRecentSession), "Ctrl-Tab behavior should be set to CycleMostLeastRecentSession" ) }) .add_assertion(save_active_window_id("first_window_id")), ); for i in 1..5 { let tab_idx = i; builder = builder .with_step( new_step_with_default_assertions( format!("Add a session {i} using cmd-t and verify it bootstraps").as_str(), ) .with_keystrokes(&[cmd_or_ctrl_shift("t")]) .set_timeout(Duration::from_secs(10)) .add_assertion(move |app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, tab_idx) }) .add_assertion(assert_tab_count(tab_idx + 1)), ) .with_step(execute_echo(tab_idx)) } builder = builder .with_step( new_step_with_default_assertions("Switch to the most recently added tab") .with_action(|app, _, data| { let window_id = match data.get("first_window_id") { Some(window_id) => *window_id, None => { panic!("Expected first_window_id to be defined"); } }; app.dispatch_custom_action(CustomAction::CycleNextSession, window_id); }), ) .with_step( new_step_with_default_assertions("release ctrl key 1") .with_event(Event::ModifierKeyChanged { key_code: KeyCode::ControlLeft, state: KeyState::Released, }) .add_assertion(assert_focused_editor_in_tab( 3, /* second to last tab, which was most recently added */ )), ) .with_step( new_step_with_default_assertions("Switch to the tab just switched away from") .with_action(|app, _, data| { let window_id = match data.get("first_window_id") { Some(window_id) => *window_id, None => { panic!("Expected first_window_id to be defined"); } }; app.dispatch_custom_action(CustomAction::CycleNextSession, window_id); }), ) .with_step( new_step_with_default_assertions("release ctrl key 2") .with_event(Event::ModifierKeyChanged { key_code: KeyCode::ControlLeft, state: KeyState::Released, }) .add_assertion(assert_focused_editor_in_tab( 4, /* last tab, which was most recently switched away from*/ )), ) .with_step( new_step_with_default_assertions("Go backwards in the tab cycle").with_action( |app, _, data| { let window_id = match data.get("first_window_id") { Some(window_id) => *window_id, None => { panic!("Expected first_window_id to be defined"); } }; app.dispatch_custom_action(CustomAction::CyclePrevSession, window_id); }, ), ) .with_step( new_step_with_default_assertions("release ctrl key 3") .with_event(Event::ModifierKeyChanged { key_code: KeyCode::ControlLeft, state: KeyState::Released, }) .add_assertion(assert_focused_editor_in_tab( 0, /* first tab, should wrap around */ )), ); builder } } // This test verifies part of the behavior that we expect from 'ssh' command pub fn test_shell_reinitializing() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Run bash") .with_typed_characters(&["bash --norc"]) .with_keystrokes(&["enter"]) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(true, 0), ) .set_timeout(Duration::from_secs(10)) .add_assertion(assert_terminal_bootstrapping(0, 0)), ) .with_step( TestStep::new("Initialize shell") .add_named_assertion("Ensure input box is visible", move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { let mut model = view.model.lock(); model.init_shell(InitShellValue { session_id: 0.into(), shell: "bash".to_string(), user: "local:user".to_owned(), hostname: "local:host".to_owned(), ..Default::default() }); let input_visible = view.is_input_box_visible(&model, ctx); async_assert!(input_visible, "Input box should be visible") }) }) .add_named_assertion("Confirm the prompt value", |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let model_arc = terminal_view.read(app, |view, _| view.model.clone()); let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, move |view, ctx| { let model = model_arc.lock(); async_assert_eq!( "Starting shell...".to_string(), view.prompt_render_helper .prompt_working_dir(&model, view.sessions(ctx)), "Checking the prompt value" ) }) }), ) } /// Verifies that ctrl-c correctly terminates long-running commands. pub fn test_ctrl_c() -> Builder { new_builder() // TODO(CORE-2734): Unknown failure for Powershell .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Run read") .add_named_assertion("no pending model events", assert_no_pending_model_events()) .with_input_string("sleep 999", Some(&["enter"])) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(true, 0), ), ) .with_step( new_step_with_default_assertions("Check ctrl-c terminates the command") .with_keystrokes(&["ctrl-c"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); async_assert!( !model .block_list() .active_block() .is_active_and_long_running(), "Check if the command has terminated" ) }) }), ) } // This is a regression test for a hang when hovering. pub fn test_hover_over_menu() -> Builder { // Test every 15x15 square. let mut hover_every_15_pixels = new_step_with_default_assertions("Hover over every 15x15 square"); for x in 0..1770 / 15 { for y in 0..1770 / 15 { hover_every_15_pixels = hover_every_15_pixels.with_event(Event::MouseMoved { position: Vector2F::new((x * 15) as f32, (y * 15) as f32), cmd: false, shift: false, is_synthetic: false, }) } } new_builder() // TODO(REV-569): Fish flaking on linux .set_should_run_test(|| { let (starter, _) = current_shell_starter_and_version(); !matches!(starter.shell_type(), ShellType::Fish) }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, String::new(), ExpectedExitStatus::Success, (), )) .with_step(hover_every_15_pixels) } // This is a regression test for hanging due to a .zshrc requiring user input // Starts zsh with a .zshrc that echoes, then issues a read call // We send a newline which should allow bootstrapping to complete pub fn test_zshrc_keypress() -> Builder { let keystrokes = vec!["\n"]; 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| { let dir = utils.test_dir(); write_rc_files_for_test(dir, "echo update\nread\n", [ShellRcType::Zsh]); }) .with_step( TestStep::new("Wait until last block is considered active & long running") .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(false, 0), ), ) .with_step( new_step_with_default_assertions("Press Enter") .with_keystrokes(&keystrokes) .set_timeout(Duration::from_secs(10)), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) } pub fn test_detect_powerlevel10k() -> Builder { fn check_banner_open(tab_index: usize, open: bool) -> TestStep { new_step_with_default_assertions("Verify incompatible configuration banner state") .add_assertion(move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, tab_index); terminal_view.read(app, |view, _ctx| { assert_eq!(view.is_incompatible_configuration_banner_open(), open); }); AssertionOutcome::Success }) } new_builder() .set_should_run_test(|| { // Powerlevel10k only supports zsh let (starter, _) = current_shell_starter_and_version(); starter.shell_type() == ShellType::Zsh }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" function _p9k_precmd () { echo "Pretending Powerlevel10k is installed" } precmd_functions+=(_p9k_precmd) "#, [ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Since honor_ps1 is not set, the banner should be closed. .with_step(check_banner_open(0, false)) // Once the custom prompt is toggled on, the banner should reopen. .with_step( new_step_with_default_assertions("Enable honor_ps1").with_action(|app, _, _| { SessionSettings::handle(app).update(app, |session_settings, ctx| { let _ = session_settings.honor_ps1.set_value(true, ctx); }); }), ) .with_step(check_banner_open(0, true)) // Additionally, a new tab should show the banner from the start. .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(check_banner_open(1, true)) // If the user then switches back to the Warp prompt, we should close the banner. .with_step( new_step_with_default_assertions("Disable honor_ps1").with_action(|app, _, _| { SessionSettings::handle(app).update(app, |session_settings, ctx| { let _ = session_settings.honor_ps1.set_value(false, ctx); }); }), ) .with_step(check_banner_open(0, false)) } pub fn test_exit_multiple_tabs() -> Builder { let (starter, _) = current_shell_starter_and_version(); // PowerShell has an "exit" keyword. However, it will not exit immediately. If there is a // background job still running, it will wait for it to finish. This means in-band commands // will block it from exiting. To get around that, we use `[System.Environment]::Exit(0)` // instead. let exit_command = match starter.shell_type() { ShellType::PowerShell => "[System.Environment]::Exit(0)", _ => "exit", }; new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Add a new session using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 1) }) .add_assertion(assert_tab_count(2)) // Wait for a render so that the view isn't removed before it's shown .set_post_step_pause(Duration::from_millis(20)), ) .with_step( new_step_with_default_assertions( "Close the session using 'exit' and verify it is closed", ) .with_typed_characters(&[exit_command]) .with_keystrokes(&["enter"]) .set_timeout(Duration::from_secs(10)) .add_assertion(assert_tab_count(1)), ) .with_step( TestStep::new("Close the remaining session using 'exit'") .with_typed_characters(&[exit_command]) .with_keystrokes(&["enter"]), ) } pub fn test_block_navigation() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Create two blocks. .with_step(execute_echo(0)) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions( "Hit cmd-up to select the block and verify it's selected", ) .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_step( new_step_with_default_assertions("Hit up arrow to navigate up.") .with_keystrokes(&["up"]) .add_assertion(assert_selected_block_index_is_first_renderable()), ) .with_step( new_step_with_default_assertions( "Hit up arrow to navigate up. It should not have moved.", ) .with_keystrokes(&["up"]) .add_assertion(assert_selected_block_index_is_first_renderable()), ) .with_step( new_step_with_default_assertions("Hit down arrow to navigate down.") .with_keystrokes(&["down"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_step( new_step_with_default_assertions( "Hit down arrow to navigate down. It should not have moved", ) .with_keystrokes(&["down"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) } pub fn test_long_running_block_height_updated() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Run python3 and verify long-running mode is activated") .with_typed_characters(&["python3"]) .with_keystrokes(&["enter"]) .add_assertion(|app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let terminal_view = views.first().unwrap(); terminal_view.read(app, |view, _| { let model = view.model.lock(); async_assert!( !model.block_list().is_empty(), "The executing block should appear" ) }) }) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(false, 0), ), ) } pub fn test_find_within_block() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo foo".to_string(), ExpectedExitStatus::Success, "foo", )) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo bar".to_string(), ExpectedExitStatus::Success, "bar", )) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo baz".to_string(), ExpectedExitStatus::Success, "baz", )) .with_step( new_step_with_default_assertions("Open Find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_find_bar_open && is_find_bar_focused, "Expect the find bar to be open and focused", ) }), ) .with_step( new_step_with_default_assertions("Type into the find box") .with_typed_characters(&["e", "c"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 3, "Expected three matches but got {:?}", num_matches ) }), ) .with_step( new_step_with_default_assertions("Click the find in block button") .with_click_on_saved_position("find_in_block_button") .add_assertion(|app, window_id| { let views: Vec>> = app.views_of_type(window_id).expect("find bar should exist"); let find_bar = views.first().unwrap(); let find_in_block_enabled = find_bar.read(app, |view, _ctx| view.display_find_within_block); async_assert!( find_in_block_enabled == FindWithinBlockState::Enabled, "Expect find in block to be turned on" ) }), ) .with_step( new_step_with_default_assertions( "Check number of matches after enabling find in block", ) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 1, "Expected one match but got {:?}", num_matches ) }), ) .with_step( new_step_with_default_assertions("Expand selection up") .with_keystrokes(&["shift-up"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 2, "Expected two matches but got {:?}", num_matches ) }), ) } pub fn test_case_sensitive_find() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo foo".to_string(), ExpectedExitStatus::Success, "foo", )) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo FOO".to_string(), ExpectedExitStatus::Success, "FOO", )) .with_step( new_step_with_default_assertions("Open Find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_find_bar_open && is_find_bar_focused, "Expect the find bar to be open and focused", ) }), ) .with_step( new_step_with_default_assertions("Type into the find box") .with_typed_characters(&["foo"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 4, "Expected four matches but got {:?}", num_matches ) }), ) .with_step( new_step_with_default_assertions("Click the case sensitivity button") .with_click_on_saved_position("case_sensitive_button") .add_assertion(|app, window_id| { let views: Vec>> = app.views_of_type(window_id).expect("find bar should exist"); let find_bar = views.first().unwrap(); let case_sensitivity_enabled = find_bar.read(app, |view, _ctx| view.case_sensitivity_enabled); async_assert!( case_sensitivity_enabled, "Expect case sensitivity to be turned on" ) }), ) .with_step( new_step_with_default_assertions( "Check number of matches after enabling case sensitivity", ) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 2, "Expected two matches but got {:?}", num_matches ) }), ) } /// Regression test for WAR-4240 pub fn test_find_bar_autoselects_text() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo foo".to_string(), ExpectedExitStatus::Success, "foo", )) .with_step( new_step_with_default_assertions("Open Find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_find_bar_open && is_find_bar_focused, "Expect the find bar to be open and focused", ) }), ) .with_step( new_step_with_default_assertions("Type into the find box") .with_typed_characters(&["e", "c"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let num_matches = terminal_view.read(app, |view, ctx| { let find_model = view.find_model().as_ref(ctx); find_model.visible_block_list_match_count() }); async_assert_eq!( num_matches, 1, "Expected one match but got {:?}", num_matches ) }), ) .with_step( new_step_with_default_assertions("Close the find bar") .with_keystrokes(&["escape"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( !is_find_bar_open && !is_find_bar_focused, "Expect the find bar to be closed", ) }), ) .with_step( new_step_with_default_assertions("Re-open the find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); terminal_view.read(app, |terminal_view, ctx| { terminal_view.find_bar().as_ref(ctx).editor().read( app, |editor_view, ctx| { let selected_text = editor_view.selected_text(ctx); async_assert!( is_find_bar_open && is_find_bar_focused && selected_text == "ec", "Expect the find bar to be open and focused with text autoselected", ) }, ) }) }), ) } pub fn test_execute_multiple_cursor_command() -> Builder { // Check that terminal does not crash upon input after multi-cursor command is run new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Set up multiple cursor command") .with_typed_characters(&["a", " ", "a"]) .with_keystrokes(&[ ADD_NEXT_OCCURRENCE_KEYBINDING, ADD_NEXT_OCCURRENCE_KEYBINDING, ]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert_eq!( view.editor().as_ref(ctx).num_selections(ctx), 2, "Check that there are 2 cursors" ) }) }), ) .with_step( new_step_with_default_assertions( "Run multiple cursor command and type first character", ) .with_keystrokes(&["enter"]) .with_typed_characters(&["a"]), ) } pub fn test_disabling_action_dispatching() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(open_command_palette()) .with_steps( open_command_palette_and_run_action("Open Keybindings Editor").add_assertion( move |app, window_id| { let settings_views: Vec> = app .views_of_type(window_id) .expect("Settings view must exist"); assert_eq!(settings_views.len(), 1); let settings_view = settings_views.first().expect("Settings view must exist"); settings_view.read(app, |view, _| { async_assert_eq!( view.current_settings_section(), SettingsSection::Keybindings ) }) }, ), ) .with_step( new_step_with_default_assertions("Click the first element in the list") .with_click_on_saved_position("first_keybinding_setting") .set_timeout(Duration::from_secs(5)) .add_assertion(move |app, window_id| { async_assert!( !app.key_bindings_dispatching_enabled(window_id), "Action dispatching is disabled" ) }), ) .with_step( new_step_with_default_assertions("Don't dispatch command palette action") .with_keystrokes(&["cmd-p"]) .set_timeout(Duration::from_secs(5)) .add_assertion(move |app, window_id| { let views: Vec> = app.views_of_type(window_id).unwrap(); let keybindings_view = views.first().unwrap(); keybindings_view.read(app, |view, _| { async_assert_eq!( &Keystroke::parse("cmd-p").unwrap(), view.rows .as_ref() .unwrap() .first() .unwrap() .binding .trigger .as_ref() .unwrap(), "Keystroke for the first binding set" ) }) }), ) .with_step( new_step_with_default_assertions( "Click the cancel button in the first keybinding editor", ) .with_click_on_saved_position("first_keybinding_cancel") .set_timeout(Duration::from_secs(5)) .add_assertion(move |app, window_id| { async_assert!( app.key_bindings_dispatching_enabled(window_id), "Action dispatching is reenabled" ) }), ) } fn create_long_block() -> TestStep { let long_echo = long_block_command(); execute_command_for_single_terminal_in_tab(0, long_echo, ExpectedExitStatus::Success, ()) } /// Returns a command that would produce a long block when executed. fn long_block_command() -> String { let lots_of_lines = (1..100).fold(String::new(), |mut last, _i| { last.push_str("a\\n"); last }); let long_echo = format!("printf \"{lots_of_lines}\""); long_echo } pub fn test_block_based_snackbar_scroll_to_top() -> Builder { // Test that clicking on the block based header scrolls to the top of the block new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(create_long_block()) .with_step( new_step_with_default_assertions("Assert scrolled down").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scroll_position = view.scroll_position(); async_assert!( matches!( scroll_position, ScrollPosition::FollowsBottomOfMostRecentBlock ), "Expected to be scrolled to bottom" ) }) }, ), ) .with_step( new_step_with_default_assertions("Click on block header") .with_click_on_saved_position("block_index:last") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scroll_position = view.scroll_position(); async_assert!( matches!(scroll_position, ScrollPosition::FixedAtPosition { .. }), "Expected to scroll up from header click" ) }) }), ) } /// Ensure that the block-based-snackbar appears when a command is running when input at bottom mode /// is enabled. pub fn test_block_based_snackbar_appears_for_running_command_input_at_bottom() -> Builder { new_builder() .with_user_defaults(user_defaults::input_mode(InputMode::PinnedToBottom)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_long_running_command( 0, format!("{} && python3", long_block_command()), )) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_visible(0)), ) } /// Ensure that the block-based-snackbar does not appear when a pager command (such as `less -X` or /// `git log`) is running when input at bottom mode is enabled. pub fn test_block_based_snackbar_not_visible_for_pager_command_input_at_bottom() -> Builder { new_builder() .with_user_defaults(user_defaults::input_mode(InputMode::PinnedToBottom)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("touch foo && echo {} >> foo", long_block_command()), ExpectedExitStatus::Success, (), )) .with_step(execute_long_running_command(0, "less -X foo".into())) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_not_visible(0)), ) } /// Ensure that the block-based-snackbar appears when a command is running when input pinned to top /// mode is enabled. pub fn test_block_based_snackbar_appears_for_running_command_pinned_to_top() -> Builder { new_builder() .with_user_defaults(user_defaults::input_mode(InputMode::PinnedToTop)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_long_running_command( 0, format!("{} && python3", long_block_command()), )) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_visible(0)), ) } /// Ensure that the block-based-snackbar does not appear when a pager command (such as `less -X` or /// `git log`) is running when input pinned to top mode is enabled. pub fn test_block_based_snackbar_not_visible_for_pager_command_pinned_to_top() -> Builder { new_builder() .with_user_defaults(user_defaults::input_mode(InputMode::PinnedToTop)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("touch foo && echo {} >> foo", long_block_command()), ExpectedExitStatus::Success, (), )) .with_step(execute_long_running_command(0, "less -X foo".into())) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_not_visible(0)), ) } /// Ensure that the block-based-snackbar appears when a command is running when input waterfall /// mode is enabled. pub fn test_block_based_snackbar_appears_for_running_command_waterfall_mode() -> Builder { new_builder() .with_user_defaults(user_defaults::input_mode(InputMode::Waterfall)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_long_running_command( 0, format!("{} && python3", long_block_command()), )) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_visible(0)), ) } /// Ensure that the block-based-snackbar does not appear when a pager command (such as `less -X` or /// `git log`) is running when input waterfall mode is enabled. pub fn test_block_based_snackbar_not_visible_pager_command_waterfall_mode() -> Builder { new_builder() // TODO(CORE-2857) There is some flakiness with long-running commands exiting. .set_should_run_test(skip_if_powershell_core_2303) .with_user_defaults(user_defaults::input_mode(InputMode::Waterfall)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("touch foo && echo {} >> foo", long_block_command()), ExpectedExitStatus::Success, (), )) .with_step(execute_long_running_command(0, "less -X foo".into())) .with_step( TestStep::new("Ensure scroll position is at bottom of most recent block") .add_assertion(assert_scroll_position( 0, ScrollPosition::FollowsBottomOfMostRecentBlock, )) .add_assertion(assert_no_pending_model_events()) .add_assertion(assert_snackbar_is_not_visible(0)), ) } pub fn test_block_based_snackbar_small_window() -> Builder { // Test that clicking on the block based header scrolls to the top of the block new_builder() .with_setup(|_utils| { integration_testing::create_file_from_assets( TEST_ONLY_ASSETS, "small_window.sqlite", &integration_testing::persistence::database_file_path_for_scope( &integration_testing::persistence::PersistenceScope::App, ), ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(create_long_block()) .with_step( new_step_with_default_assertions("Assert scrolled down").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scroll_position = view.scroll_position(); async_assert!( matches!( scroll_position, ScrollPosition::FollowsBottomOfMostRecentBlock ), "Expected to be scrolled to bottom" ) }) }, ), ) .with_step( new_step_with_default_assertions("Click on block header") .with_click_on_saved_position("block_index:last") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let scroll_position = view.scroll_position(); async_assert!( matches!( scroll_position, ScrollPosition::FollowsBottomOfMostRecentBlock ), "Expected no scroll from clicking on header" ) }) }), ) } pub fn test_multi_block_selections() -> Builder { // Check that multi block selections work as expected new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_echo(0)) .with_step(execute_echo(0)) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Expand selection up") .with_keystrokes(&["cmdorctrl-up", "shift-up"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); let last_index = model .block_list() .last_non_hidden_block_by_index() .expect("block should exist"); let second_last_index = model .block_list() .prev_non_hidden_block_from_index(last_index) .expect("block_should_exist"); let pivot_index = view.selected_blocks_pivot_index(); let tail_index = view.selected_blocks_tail_index(); assert_eq!(pivot_index.unwrap(), last_index); assert_eq!(tail_index.unwrap(), second_last_index); AssertionOutcome::Success }) }), ) .with_step( new_step_with_default_assertions("Expand selection back down") .with_keystrokes(&["shift-down"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); let last_index = model .block_list() .last_non_hidden_block_by_index() .expect("block should exist"); let pivot_index = view.selected_blocks_pivot_index(); let tail_index = view.selected_blocks_tail_index(); assert_eq!(pivot_index.unwrap(), last_index); assert_eq!(tail_index.unwrap(), last_index); AssertionOutcome::Success }) }), ) } /// Bash commonly comes with .bashrc files that will no-op if PS1 is not set. /// This test includes an alias for `l` that's set if PS1 is set, and will /// fail if `l` is not set. pub fn test_alias_guards_on_ps1_set() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash and zsh let (starter, _) = current_shell_starter_and_version(); matches!( starter.shell_type(), shell::ShellType::Zsh | shell::ShellType::Bash ) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" # If not running interactively, don't do anything [ -z "$PS1" ] && return alias l='ls -CF' "#, [ShellRcType::Bash, ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, /*tab_idx*/ "l".to_string(), ExpectedExitStatus::Success, (), )) } /// Regression test that ensures that if we don't set any PS1 value, the value /// that comes from the binary is preserved. /// The exit value is equivalent to empty string for older versions of bash. This /// value comes from running the subshell and parsing the output, if the PS1 is empty. pub fn test_ps1_value_not_null_or_exit() -> Builder { let (starter, _) = current_shell_starter_and_version(); let check_exit = matches!(starter.shell_type(), ShellType::Bash); new_builder() .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( // Check the PS1 value of the hidden block after we've finished bootstrapping. // This will be the block created at the `PostBootstrapPrecmd` stage. 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); 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"); let prompt = block.prompt_to_string(); if check_exit { async_assert!( !prompt.is_empty() && !prompt.contains("exit"), "Prompt should not contain the string \"exit\"" ) } else { async_assert!(!prompt.is_empty(), "Prompt should not be empty") } }) }, ), ) } /// Regression test that ensures we support custom prompts with bash, whether /// it's a recent version or the older version that ships with macOS. /// The system bash includes a hardcoded deprecation warning, which caused a /// prompt expansion bug. pub fn test_custom_ps1_expansion_bash() -> Builder { new_builder() .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .set_should_run_test(|| { let (starter, _) = current_shell_starter_and_version(); starter.shell_type() == ShellType::Bash }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, // Use a prompt special character, \$, to make sure PS1 is expanded r#"export PS1="prompt-for-test: \$""#, [ShellRcType::Bash], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Check PS1 value").add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); let prompt = model .block_list() .active_block() .prompt_contents_to_string(false); assert_eq!(&prompt, "prompt-for-test: $", "Unexpected PS1: {prompt:?}"); AssertionOutcome::Success }) }), ) } /// Default auto title. We test that Warp's auto title is used and verify that /// DISABLE_AUTO_TITLE is set correctly. pub fn test_auto_title() -> 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)) .with_step(tab_title_step( "Assert the default tab title used", "~".to_string(), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo $DISABLE_AUTO_TITLE".to_string(), ExpectedExitStatus::Success, util::per_shell_output(vec![(shell::ShellType::Zsh, "true")]), )) } /// Validate that disabling Warp's auto title feature will not mess with oh-my-zsh settings. pub fn test_warp_auto_title_disabled() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash and zsh let (starter, _) = current_shell_starter_and_version(); matches!( starter.shell_type(), shell::ShellType::Zsh | shell::ShellType::Bash ) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, r#" GALAXY_DISABLE_AUTO_TITLE="1" "#, [ShellRcType::Bash], ); write_rc_files_for_test( &dir, r#" GALAXY_DISABLE_AUTO_TITLE="true" "#, [ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // If Warp title is disabled, we don't set the DISABLE_AUTO_TITLE env variable .with_step(execute_command_for_single_terminal_in_tab( 0, "echo $DISABLE_AUTO_TITLE".to_string(), ExpectedExitStatus::Success, util::per_shell_output(vec![ (shell::ShellType::Zsh, ""), (shell::ShellType::Bash, ""), ]), )) } /// Checks that the tab title set by the user takes precedence over the Warp's default title and /// doesn't require any additional setting from the user's POV. This is bash-specific test. pub fn test_warp_honors_user_title_bash() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash let (starter, _) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Bash) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" PROMPT_COMMAND='echo -en "\033]0;TEST_TAB_TITLE\a"' "#, [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(), )) } /// Checks that the tab title set by the user takes precedence over the Warp's default title and /// doesn't require any additional setting from the user's POV. This is zsh-specific test. pub fn test_warp_honors_user_title_zsh() -> Builder { new_builder() .set_should_run_test(|| { // Only run this one on bash let (starter, _) = current_shell_starter_and_version(); matches!(starter.shell_type(), shell::ShellType::Zsh) }) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" function set_title () { window_title="\033]0;TEST_TAB_TITLE\007" echo -ne "$window_title" } precmd_functions+=(set_title) "#, [ShellRcType::Zsh], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(tab_title_step( "Assert the user's tab title used", "TEST_TAB_TITLE".to_string(), )) } /// Checks that an OSC 7 escape sequence (`\e]7;file://host/path\a`) emitted by /// the running command updates the block's current working directory mid-command /// without waiting for the next prompt. This lets external tools that change /// directory (for example `wt switch` from worktrunk) keep Warp's per-block CWD /// in sync with the shell. See issue #9125. pub fn test_osc7_updates_current_working_directory() -> Builder { new_builder() .set_should_run_test(|| { let (starter, _) = current_shell_starter_and_version(); matches!( starter.shell_type(), shell::ShellType::Bash | shell::ShellType::Zsh ) }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(clear_blocklist_to_remove_bootstrapped_blocks()) .with_step(execute_command_for_single_terminal_in_tab( 0, /*tab_idx*/ // OSC 7 is only honored when the host portion matches the local // hostname; substitute it dynamically so the test works on any // machine. r#"mkdir -p /tmp/osc7-test && cd /tmp/osc7-test && printf "\033]7;file://$(hostname)/tmp/osc7-test\a""#.to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Assert OSC 7 updated the previous block's pwd") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { let model = view.model.lock(); let last_finished_pwd = model .block_list() .blocks() .iter() .rev() .find(|block| block.finished() && !block.is_background()) .and_then(|block| block.pwd().cloned()); async_assert_eq!( Some("/tmp/osc7-test".to_string()), last_finished_pwd, "expected OSC 7 to set the printf block's pwd to /tmp/osc7-test" ) }) }) // Locks in the fix for the user-visible regression Zach saw: // the `WorkingDirectory` prompt chip text (read by // `display_working_directory`, which feeds the vertical-tab // subtitle) must be refreshed after OSC 7. Without // `refresh_warp_prompt` in the `BlockWorkingDirectoryUpdated` // path, the block's `pwd` updates but the chip text stays on // the old CWD until the next `BlockCompleted`. .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { let displayed = view.display_working_directory(ctx); async_assert_eq!( Some("/tmp/osc7-test".to_string()), displayed, "expected the WorkingDirectory chip / tab subtitle to reflect the OSC 7 CWD" ) }) }), ) } /// Checks that we focus the prompt after executing a command, regardless /// of if the find bar is open or not. pub fn test_input_focused_after_executing_command() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Open Find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_find_bar_open && is_find_bar_focused, "Expect the find bar to be open and focused", ) }), ) .with_step( new_step_with_default_assertions("Click input editor and verify input box is focused") .with_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("input_editor_{}", input.id()) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box should be focused" ) }) }) }), ) .with_step(execute_echo(0)) .with_step( new_step_with_default_assertions("Make sure correct view is focused").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_input_focused = terminal_view.read(app, |view, ctx| { view.input().as_ref(ctx).editor().as_ref(ctx).is_focused() }); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_input_focused && !is_find_bar_focused, "Expect the input editor to be focused" ) }, ), ) } /// Regression test to ensure that a new session results in the input box /// being focused immediately. pub fn test_new_session_focuses_input() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Input box should be focused").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { let is_input_focused = view.input().as_ref(ctx).editor().as_ref(ctx).is_focused(); assert!(is_input_focused); }); AssertionOutcome::Success }, ), ) } /// TODO: make this test work for fish as well pub fn test_executable_completions() -> Builder { new_builder() .set_should_run_test(|| { let (starter, _version) = current_shell_starter_and_version(); // TODO(CORE-2734): Unknown failure for Powershell !matches!(starter.shell_type(), ShellType::Fish) && skip_if_powershell_core_2303() }) .with_setup(|utils| { let dir = utils.test_dir(); let dir_string = dir .to_str() .expect("Should be able to convert test dir to str"); let filepath = format!("{dir_string}/my_exec"); let rc_contents = format!( r#" touch {filepath} && chmod a+x {filepath}; PATH=$PATH:{dir_string} "# ); write_rc_files_for_test(&dir, rc_contents, [ShellRcType::Bash, ShellRcType::Zsh]); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Wait until executables are loaded") .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert executables are loaded", move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, ctx| { let active_session_id = view.active_block_session_id().unwrap(); let session = view.sessions(ctx).get(active_session_id).unwrap(); async_assert!( session.external_commands().get().is_some(), "External commands not loaded yet" ) }) }), ) .with_step( new_step_with_default_assertions("Enter 'm' and hit tab") .with_typed_characters(&["m"]) .with_keystrokes(&["tab"]) .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert executable completions", move |app, window_id| { let input_suggestions = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions.read(app, |view, _ctx| { async_assert!( view.items().iter().any(|item| item.text() == "my_exec"), "my_exec should be suggested" ) }) }), ) } // TODO: we should test this for fish someday too. pub fn test_function_completions() -> Builder { new_builder() .set_should_run_test(|| { let (starter, _version) = current_shell_starter_and_version(); // TODO(CORE-2734): Unknown failure for Powershell !matches!(starter.shell_type(), ShellType::Fish) && skip_if_powershell_core_2303() }) .with_setup(|utils| { let dir = utils.test_dir(); let content = "my_func () { true ; }"; write_rc_files_for_test(dir, content, [ShellRcType::Bash, ShellRcType::Zsh]); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Enter 'm' and hit tab") .with_typed_characters(&["m"]) .with_keystrokes(&["tab"]) .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert function completions", move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { let function_name = "my_func"; let buffer_text = view.buffer_text(ctx); let suggestion_inserted_into_buffer = buffer_text.trim() == function_name; let suggestion_in_tab_menu = match view.suggestions_mode_model().as_ref(ctx).mode() { InputSuggestionsMode::CompletionSuggestions { completion_results, .. } => completion_results .suggestions .iter() .any(|item| item.display() == function_name), _ => false, }; // If "my_func" was the only result, then it would be inserted directly, otherwise // it should be in the tab completions menu. async_assert!( suggestion_inserted_into_buffer || suggestion_in_tab_menu, "my_func is not a tab completion result" ) }) }), ) } pub fn test_builtin_completions() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Enter 's' and hit tab") .with_typed_characters(&["s"]) .with_keystrokes(&["tab"]) .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert builtin completions work", move |app, window_id| { let input_suggestions = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions.read(app, |view, _ctx| { async_assert!( view.items().iter().any(|item| item.text() == "set"), "set should be suggested" ) }) }), ) } pub fn test_keyword_completions() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Enter 'w' and hit tab") .with_typed_characters(&["w"]) .with_keystrokes(&["tab"]) .set_timeout(Duration::from_secs(30)) .add_named_assertion("Assert keyword completions work", move |app, window_id| { let input_suggestions = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions.read(app, |view, _ctx| { async_assert!( view.items().iter().any(|item| item.text() == "while"), "while should be suggested" ) }) }), ) } pub fn test_add_windows_correct_position_and_cascade() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Assert we have only 1 window open at start") .add_assertion(move |app, _| async_assert_eq!(app.window_ids().len(), 1)) .add_assertion(save_active_window_id("first_window_id")), ) // Shrink the first window so there is room on screen for the cascade // offset. Without this, screens that are close to the default window // size leave no room and cascadeTopLeftFromPoint: is a no-op. .with_step( new_step_with_default_assertions("Resize first window to leave room for cascade") .with_action(|app, window_id, _| { app.read(|ctx| { let window_manager = WindowManager::as_ref(ctx); let bounds = ctx.window_bounds(&window_id).expect("window should have bounds"); let bounds = RectF::new( bounds.origin(), Vector2F::new(640., 480.), ); window_manager.set_window_bounds(window_id, bounds); }); }), ) .with_step(add_window(2) .add_assertion(save_active_window_id("second_window_id"))) .with_step(new_step_with_default_assertions("Assert cascading windows") .add_named_assertion_with_data_from_prior_step("Check that second window is positioned with cascade", move |app, window_id, data| { let first_window_id = match data.get("first_window_id") { Some(window_id) => *window_id, None => { return AssertionOutcome::failure("Expected first_window_id to be defined".into()); } }; let first_window_bounds = app.window_bounds(&first_window_id).expect("first window bounds defined"); // Save the first window bounds for a later step data.insert("first_window_bounds", first_window_bounds); let second_window_bounds = app.window_bounds(&window_id).expect("second window bounds defined"); async_assert!(first_window_id != window_id && second_window_bounds.origin_x() > first_window_bounds.origin_x() && second_window_bounds.origin_y() > first_window_bounds.origin_y(), "Second window should below and right of first window but second window was {:?} and first window was {:?}", second_window_bounds, first_window_bounds) } )) .with_step(close_window("first_window_id", 1)) .with_step(add_window_and_check_bounds(2, "first_window_bounds")) } pub fn test_open_new_tab_with_specific_shell_from_new_session_menu() -> Builder { FeatureFlag::ShellSelector.set_enabled(true); // Consults the AvailableShells model to find a shell based on the shell type, // gets the display name, and then clicks on that entry in the new session menu. fn new_tab_with_click_on_shell(shell: ShellType) -> TestStep { let shell_name = shell.name(); new_step_with_default_assertions(format!("Open New tab with {shell_name} shell").as_str()) .with_click_on_saved_position_fn(move |app, _| { AvailableShells::handle(app).read(app, |shells, _| { let shell = shells.find_known_shell_by_type(shell).unwrap_or_else(|| { panic!("Shell {shell_name} should be loaded into AvailableShells") }); shells.display_name_for_shell(&shell).to_string() }) }) } let test_cases = vec![(ShellType::PowerShell, "(Get-Process -Id $PID).Path")]; let mut builder = new_builder() .set_should_run_test(|| cfg!(windows)) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)); for (tab_index, (shell, test_command)) in (1..).zip(test_cases) { let expected = regex::Regex::new(format!("{}$", shell.name()).as_str()).expect("regex should compile"); builder = builder .with_step( new_step_with_default_assertions("Click on new tab menu button") .with_click_on_saved_position(NEW_SESSION_MENU_BUTTON_POSITION_ID), ) .with_step(new_tab_with_click_on_shell(shell)) .with_step( wait_until_bootstrapped_single_pane_for_tab(tab_index) .set_timeout(Duration::from_secs(20)), ) .with_step(execute_command_for_single_terminal_in_tab( tab_index, test_command.to_string(), ExpectedExitStatus::Success, expected, )); } builder } pub fn test_command_xray_hover() -> Builder { new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Type in command") .with_typed_characters(&["git status"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { view.editor().read(app, |editor, _ctx| { assert!(editor.get_command_x_ray().is_none()) }); async_assert!( view.buffer_text(ctx) == "git status", "Expect the buffer text to be correct" ) }) }), ) .with_step( new_step_with_default_assertions("Hover over status and make sure tooltip shows") .with_event_fn(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.update(app, |view, ctx| { let mut position = ctx .element_position_by_id(format!("editor:cursor_{}", view.editor().id())) .expect("editor cursor should have a position") .origin(); // Move the position slightly left and down so it's clearly over the "status" token position.set_x(position.x() - 10.); position.set_y(position.y() + 5.); Event::MouseMoved { position, cmd: false, shift: false, is_synthetic: false, } }) }) .add_assertion(move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, _ctx| { view.editor().read(app, |editor, _ctx| { async_assert!( editor.get_command_x_ray().is_some(), "Command XRay state should be set" ) }) }) }) .set_timeout(Duration::from_secs(5)), ) .with_step( new_step_with_default_assertions("Hover past buffer text") // Add post step pause so that the async assert in the next // step doesn't succeed right away just because we didn't give enough // time for the xray to trigger. .set_post_step_pause(Duration::from_secs(1)) .with_event_fn(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.update(app, |view, ctx| { let mut position = ctx .element_position_by_id(format!("editor:cursor_{}", view.editor().id())) .expect("editor cursor should have a position") .origin(); // Move the position slightly right so it's clearly past the buffer text position.set_x(position.x() + 10.); Event::MouseMoved { position, cmd: false, shift: false, is_synthetic: false, } }) }), ) .with_step( new_step_with_default_assertions("Make sure tooltip doesn't show") .add_assertion(move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, _ctx| { view.editor().read(app, |editor, _ctx| { async_assert!( editor.get_command_x_ray().is_none(), "Command XRay state should not be set" ) }) }) }) .set_timeout(Duration::from_secs(5)), ) } /// Regression test for WAR-4951 pub fn test_command_xray_for_partial_command() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Type in partial command") .with_typed_characters(&["git st"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { view.editor().read(app, |editor, _ctx| { assert!(editor.get_command_x_ray().is_none()) }); async_assert!( view.buffer_text(ctx) == "git st", "Expect the buffer text to be correct" ) }) }), ) .with_step( new_step_with_default_assertions("Hover over st") // Add post step pause so that the async assert in the next // step doesn't succeed right away just because we didn't give enough // time for the xray to trigger. .set_post_step_pause(Duration::from_secs(1)) .with_event_fn(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.update(app, |view, ctx| { let mut position = ctx .element_position_by_id(format!("editor:cursor_{}", view.editor().id())) .expect("editor cursor should have a position") .origin(); // Move the position slightly left and down so it's clearly over the "status" token position.set_x(position.x() - 10.); position.set_y(position.y() + 5.); Event::MouseMoved { position, cmd: false, shift: false, is_synthetic: false, } }) }), ) .with_step( new_step_with_default_assertions("Make sure tooltip doesn't show") .add_assertion(move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, _ctx| { view.editor().read(app, |editor, _ctx| { async_assert!( editor.get_command_x_ray().is_none(), "Command XRay state should not be set" ) }) }) }) .set_timeout(Duration::from_secs(5)), ) } /// Regression test for WAR-4288 pub fn test_ctrl_r_multi_cursor() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Set up multiple cursors") .with_keystrokes(&["l", "\n", "s", "ctrl-shift-up"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert_eq!( view.editor().as_ref(ctx).num_selections(ctx), 2, "Check that there are 2 cursors" ) }) }), ) .with_step( new_step_with_default_assertions("Run ctrl-r and make sure it succeeds") .with_keystrokes(&["ctrl-r"]), ) } /// This test ensures that the HISTCONTROL env var is not clobbered by our bootstrap process for bash. /// See https://linear.app/warpdotdev/issue/WAR-2592 for more details pub fn test_histcontrol_env_var() -> Builder { let histcontrol_val = "ignorespace"; new_builder() .set_should_run_test(|| { let (starter, _) = current_shell_starter_and_version(); matches!(starter.shell_type(), ShellType::Bash) }) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, format!("export HISTCONTROL={histcontrol_val}"), [ShellRcType::Bash], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "echo $HISTCONTROL".to_string(), ExpectedExitStatus::Success, histcontrol_val, )) } pub fn test_session_navigation_recency_change_tab() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Add a second session using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that second tab is most recent.") .add_assertion(move |app, window_id| { let first_tab_session = single_terminal_pane_view_for_tab(app, window_id, 0); let second_tab_session = single_terminal_pane_view_for_tab(app, window_id, 1); check_recency( first_tab_session, second_tab_session, RecentSession::Second, app, window_id, ) }), ) .with_step(close_command_palette()) .with_step( new_step_with_default_assertions("Navigate to previous tab.") .with_per_platform_keystroke(PerPlatformKeystroke { mac: "shift-cmd-{", linux_and_windows: "ctrl-pageup", }) .add_assertion(move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box of original tab should be focused" ) }) }) }), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that first tab is most recent.").add_assertion( move |app, window_id| { let first_tab_session = single_terminal_pane_view_for_tab(app, window_id, 0); let second_tab_session = single_terminal_pane_view_for_tab(app, window_id, 1); check_recency( first_tab_session, second_tab_session, RecentSession::First, app, window_id, ) }, ), ) } pub fn test_session_navigation_recency_navigate_to_tab() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Open a new tab using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) .with_step(open_navigation_palette_step()) .with_step(navigate_to_other_session_step()) .with_step( new_step_with_default_assertions("Check that first tab's input box is focused.") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box of original tab should be focused" ) }) }) }), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that first tab is most recent.").add_assertion( move |app, window_id| { let first_tab_session = single_terminal_pane_view_for_tab(app, window_id, 0); let second_tab_session = single_terminal_pane_view_for_tab(app, window_id, 1); check_recency( first_tab_session, second_tab_session, RecentSession::First, app, window_id, ) }, ), ) } pub fn test_session_navigation_recency_click_on_window() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Opening a new window") .with_action(move |app, _, _| { app.dispatch_global_action("root_view:open_new", ()); }) .add_assertion(move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box of new window should be focused" ) }) }) }) .add_assertion(move |app, _| { let window_id = app.read(|ctx| ctx.windows().active_window()); AssertionOutcome::SuccessWithData(StepData::new( "first_window_id", window_id.expect("window id present"), )) }) .set_timeout(Duration::from_secs(10)), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that second window session is most recent.") .add_named_assertion_with_data_from_prior_step( "Check that second window session is most recent", move |app, window_id, data| { assert_eq!(app.window_ids().len(), 2, "Should have 2 windows open"); let Some(first_window_id) = data.get("first_window_id") else { return AssertionOutcome::failure( "Expected window id to be passed from prior step".to_owned(), ); }; let first_window_session = single_terminal_pane_view_for_tab(app, *first_window_id, 0); let second_window_session = single_terminal_pane_view_for_tab(app, window_id, 0); check_recency( first_window_session, second_window_session, RecentSession::Second, app, window_id, ) }, ), ) .with_step(close_command_palette()) .with_step( new_step_with_default_assertions("Click on the first window to focus it.") .set_timeout(Duration::from_secs(10)) .with_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("prompt_area_{}", input.id()) }) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box should be focused." ) }) }) }), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that first window session is most recent.") .add_named_assertion_with_data_from_prior_step( "first window is most recent", move |app, window_id, data| { let Some(first_window_id) = data.get("first_window_id") else { return AssertionOutcome::failure( "Expected window id to be passed from prior step".to_owned(), ); }; let first_window_session = single_terminal_pane_view_for_tab(app, *first_window_id, 0); let second_window_session = single_terminal_pane_view_for_tab(app, window_id, 0); check_recency( first_window_session, second_window_session, RecentSession::First, app, window_id, ) }, ), ) } pub fn test_session_navigation_recency_navigate_to_window() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Opening a new window") .with_action(move |app, _, _| { app.dispatch_global_action("root_view:open_new", ()); }) .add_assertion(move |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box of new window should be focused" ) }) }) }) .add_assertion(save_active_window_id("first_window_id")) .set_timeout(Duration::from_secs(10)), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that second window session is most recent.") .add_named_assertion_with_data_from_prior_step( "first window is most recent", move |app, window_id, data| { let Some(first_window_id) = data.get("first_window_id") else { return AssertionOutcome::failure( "Expected window id to be passed from prior step".to_owned(), ); }; let first_window_session = single_terminal_pane_view_for_tab(app, *first_window_id, 0); let second_window_session = single_terminal_pane_view_for_tab(app, window_id, 0); check_recency( first_window_session, second_window_session, RecentSession::Second, app, window_id, ) }, ), ) .with_step( navigate_to_other_session_step().add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { view.input().read(app, |input, ctx| { async_assert!( input.editor().is_focused(ctx), "Input box of original window should be focused" ) }) }) }), ) .with_step(open_navigation_palette_step()) .with_step( new_step_with_default_assertions("Check that first window session is most recent.") .add_named_assertion_with_data_from_prior_step( "first window is most recent", move |app, window_id, data| { let Some(first_window_id) = data.get("first_window_id") else { return AssertionOutcome::failure( "Expected window id to be passed from prior step".to_owned(), ); }; let first_window_session = single_terminal_pane_view_for_tab(app, *first_window_id, 0); let second_window_session = single_terminal_pane_view_for_tab(app, window_id, 0); check_recency( first_window_session, second_window_session, RecentSession::First, app, window_id, ) }, ), ) } pub fn test_accepting_completion_inserts_space() -> Builder { new_builder() .with_setup(|utils| { // Change dir to cargo test dir for auto cleanup. let dir = utils.test_dir(); let dir_string = dir .to_str() .expect("Should be able to convert test dir to str"); write_rc_files_for_test( &dir, format!("cd {dir_string}"), [ShellRcType::Bash, ShellRcType::Zsh, ShellRcType::Fish], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir test test2 && touch test/abc test/abd".to_string(), ExpectedExitStatus::Success, (), )) .with_step(run_completer(0, "mv test")) .with_step( new_step_with_default_assertions( "Hit tab to select the first item and enter to accept directory suggestion", ) .with_keystrokes(&["tab", "enter"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); // Ensure we did NOT append a space at the end input_view.read(app, |input, ctx| { let buffer_text = input.buffer_text(ctx); // Ensure we did NOT append a space at the end assert_eq!(buffer_text, "mv test/".to_owned()); assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::Closed )); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions("Open tab completions menu and fill up to prefix") .with_typed_characters(&["a"]) .with_keystrokes(&["tab"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input, ctx| { let buffer_text = input.buffer_text(ctx); // Ensure we did NOT append a space at the end assert_eq!(buffer_text, "mv test/ab".to_owned()); assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } )); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions("Accepting suggestion should add space at end") .with_keystrokes(&["tab", "enter"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input, ctx| { let buffer_text = input.buffer_text(ctx); assert_eq!(buffer_text, "mv test/abc ".to_owned()); assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::Closed )); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions("Tab right away to see completions menu again") .with_keystrokes(&["tab"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input, ctx| { assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } )); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions("Move cursor away from end of buffer and hit tab") .with_keystrokes(&["backspace", "left", "tab"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input, ctx| { let buffer_text = input.buffer_text(ctx); assert_eq!(buffer_text, "mv test/abc".to_owned()); assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } )); }); AssertionOutcome::Success }), ) .with_step( new_step_with_default_assertions( "Accepting tab completion in middle of buffer doesn't append space", ) .with_keystrokes(&["tab", "enter"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input, ctx| { let buffer_text = input.buffer_text(ctx); assert_eq!(buffer_text, "mv test/abcc".to_owned()); assert!(matches!( input.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::Closed )); }); AssertionOutcome::Success }), ) } pub fn test_create_session_with_split_pane_while_bootstrapping() -> Builder { // cd to the test's tmp directory so we can test pwd preservation from parent to child // session while the parent is still bootstrapping -- we just need to cd to any non-home // directory. let test_dir = PathBuf::from(cargo_target_tmpdir::get()); new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!( "cd {}", test_dir .to_str() .expect("Test temp directory path should be UTF-8 compatible.") ), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions( "Create two new sessions in two split panes, with the second being created before \ the first is done bootstrapping.", ) .with_keystrokes(&[cmd_or_ctrl_shift("d")]) .with_keystrokes(&[cmd_or_ctrl_shift("d")]), ) .with_step( wait_until_bootstrapped_single_pane_for_tab(1) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 1) }), ) .with_step( wait_until_bootstrapped_single_pane_for_tab(2) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 2) }), ) .with_step( new_step_with_default_assertions( "Check working directories of newly created sessions.", ) .set_timeout(Duration::from_secs(10)) .add_assertion(move |app, window_id| { let views = app .views_of_type(window_id) .expect("App has no open window."); let workspace: &ViewHandle = views.first().expect("Window is missing Workspace view."); let test_dir_ref: &PathBuf = &test_dir; workspace.read(app, |workspace, ctx| { workspace .get_pane_group_view(0) .expect("Workspace has no tab view.") .read(ctx, |pane_group, ctx| { for pane_index in 0..=2 { let actual_path = pane_group .terminal_view_at_pane_index(pane_index, ctx) .unwrap_or_else(|| { panic!( "Pane group is missing pane view at index {pane_index}" ) }) .read(ctx, |view, ctx| view.active_session_path_if_local(ctx)); if !matches!(actual_path, Some(path) if &path == test_dir_ref) { return AssertionOutcome::failure(format!( "Pane {pane_index} was not in expected path {}", test_dir_ref.display() )); } } AssertionOutcome::Success }) }) }), ) } pub fn test_create_session_with_new_tab_while_bootstrapping() -> Builder { // cd to the test's tmp directory so we can test pwd preservation from parent to child // session while the parent is still bootstrapping -- we just need to cd to any non-home // directory. let test_dir = PathBuf::from(cargo_target_tmpdir::get()); new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!( "cd {}", test_dir .as_os_str() .to_str() .expect("Test temp directory path should be UTF-8 compatible.") ), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions( "Create two new sessions in two new tabs, with the second being created before \ the first is done bootstrapping.", ) .with_keystrokes(&[cmd_or_ctrl_shift("t")]) .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step( wait_until_bootstrapped_single_pane_for_tab(1) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 1) }), ) .with_step( wait_until_bootstrapped_single_pane_for_tab(2) .set_timeout(Duration::from_secs(10)) .add_assertion(|app, window_id| { assert_single_terminal_in_tab_bootstrapped(app, window_id, 2) }), ) .with_step( new_step_with_default_assertions( "Check working directories of newly created sessions.", ) .set_timeout(Duration::from_secs(10)) .add_assertion(move |app, window_id| { let views = app .views_of_type(window_id) .expect("App has no open window."); let workspace: &ViewHandle = views.first().expect("Window is missing Workspace view."); let test_dir_ref: &PathBuf = &test_dir; workspace.read(app, |workspace, ctx| { for tab_view in workspace.tab_views() { let tab_path = tab_view.read(ctx, |pane_group, ctx| { pane_group .terminal_view_at_pane_index(0, ctx) .unwrap_or_else(|| panic!("Pane group is missing terminal view.")) .read(ctx, |view, ctx| view.active_session_path_if_local(ctx)) }); if !matches!(tab_path, Some(path) if &path == test_dir_ref) { return AssertionOutcome::failure(format!( "Tab was not in expected path {}", test_dir_ref.display() )); } } AssertionOutcome::Success }) }), ) } pub fn test_cmd_enter() -> Builder { new_builder() .set_should_run_test(|| cfg!(target_os = "macos")) .with_setup(|utils| { // Change dir to cargo test dir for auto cleanup. let dir = utils.test_dir(); let dir_string = dir .to_str() .expect("Should be able to convert test dir to str"); write_all_rc_files_for_test(&dir, format!("cd {dir_string}")); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "touch test1".into(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "touch test2".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions( "Cmd+enter on closed suggestions menu, no action - current input not executed", ) .with_typed_characters(&["cd"]) .with_keystrokes(&[cmd_or_ctrl_shift("enter")]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!(!view.suggestions_mode_model().as_ref(ctx).mode().is_visible(), "Suggestions menu should be closed") }) }) .add_assertion(assert_command_executed_for_single_terminal_in_tab(0, "touch test2".to_string())), ) .with_step( new_step_with_default_assertions( "Clear buffer", ) .with_keystrokes(&[ // ATODO: change this to ctrl-a cmd_or_ctrl_shift("a").as_str(), "backspace" // delete existing buffer text ]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( view.buffer_text(ctx).is_empty(), "Expected buffer text to be empty, got {}", view.buffer_text(ctx) ) }) }), ) .with_step( new_step_with_default_assertions( "Enter up key and verify previous command is in buffer", ) .with_keystrokes(&[ "up", "up", // second last item in history menu ]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { let history_menu_open = matches!( view.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::HistoryUp { .. } ); let buffer_text_is_last_executed_command = view.buffer_text(ctx) == *"touch test1"; async_assert!( history_menu_open && buffer_text_is_last_executed_command, "Expected history menu to be open, got {}, and last executed command {} to be in buffer, instead got {}", history_menu_open, "touch test1", view.buffer_text(ctx) ) }) }), ) .with_step( new_step_with_default_assertions("Cmd+enter on history item, command is executed") .with_keystrokes(&[cmd_or_ctrl_shift("enter")]) .add_assertion(assert_command_executed_for_single_terminal_in_tab(0, "touch test1".to_string())) .add_assertion(assert_active_block_received_precmd(0, 0)) ) .with_step( new_step_with_default_assertions("Enter `ls t` and hit tab") .with_typed_characters(&["ls t"]) // Tab once to complete up to the matching prefix and open completions menu .with_keystrokes(&["tab"]) .add_named_assertion( "Assert tab completions menu opens", move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { let tab_completions_menu_open = matches!( view.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } ); async_assert!( tab_completions_menu_open, "Expected tab completions menu to be open, got {:?}", view.suggestions_mode_model().as_ref(ctx).mode(), ) }) }, ) ) .with_step( new_step_with_default_assertions("Tab again to select the first item") .with_keystrokes(&["tab"]) .add_named_assertion( "Assert test1 command is selected", move |app, window_id| { let input_suggestions = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions.read(app, |view, _ctx| { async_assert!( view.items().iter().any(|item| item.text() == "test1"), "test1 should be selected" ) }) }, ), ) .with_step( new_step_with_default_assertions( "Cmd+enter on tab completions item, command is executed", ) .with_keystrokes(&[cmd_or_ctrl_shift("enter")]) .add_assertion(assert_command_executed_for_single_terminal_in_tab(0, "ls test1".to_string())), ) } pub fn test_completions_as_you_type() -> Builder { new_builder() .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir.clone(), // We need two entries that match the 'gitte' prefix to test tab behavior "alias gittest='git'\nalias gittext='git'", [ShellRcType::Bash, ShellRcType::Zsh, ShellRcType::Fish], ); write_rc_files_for_test(dir, "New-Alias gittest git", [ShellRcType::PowerShell]); }) .with_user_defaults(HashMap::from([( CompletionsOpenWhileTyping::storage_key().to_string(), true.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("One letter is too short to open the menu") .with_typed_characters(&["g"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert_eq!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::Closed, "InputSuggestions should be closed after inserting 'g'" ) }) }), ) .with_step( new_step_with_default_assertions("Two letters is long enough to open the menu") .with_typed_characters(&["i"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be open after inserting 'i'" ) }) }), ) .with_step( new_step_with_default_assertions("Type tte to narrow down to two results") .with_typed_characters(&["tte"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } ), "Tab completions are not open after typing 'tte'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { let first_item = view.items().first().expect("should be Some()").text(); async_assert_eq!( first_item, "gittest", "Expected first completion result to be 'gittest' but got '{first_item}'" ) }) }), ) .with_step( new_step_with_default_assertions( "Hitting tab should select the first item in the menu and insert result into buffer", ) .with_keystrokes(&["tab"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { assert_eq!(view.buffer_text(ctx), "gittest".to_owned()); async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should remain open after selecting 'gittest'" ) }) }), ) .with_step( new_step_with_default_assertions( "Hitting enter should close the menu and add a space", ) .with_keystrokes(&["enter"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { assert_eq!(view.buffer_text(ctx), "gittest ".to_owned()); async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::Closed ), "InputSuggestions should be closed after accepting 'gittest'" ) }) }), ) .with_step( new_step_with_default_assertions("Space should reopen the menu") .with_typed_characters(&[" "]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be open after adding space" ) }) }), ) .with_step( new_step_with_default_assertions( "Get close to an exact match between suggestion and buffer text", ) .with_typed_characters(&["commi"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be typing 'commi'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { async_assert_eq!( view.items().first().expect("should be Some()").text(), "commit", "First completion result is not 'commit'" ) }) }), ) } pub fn test_completions_as_you_type_one_matching_entry_tab() -> Builder { new_builder() .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir.clone(), "alias gittest='git'", [ShellRcType::Bash, ShellRcType::Zsh, ShellRcType::Fish], ); write_rc_files_for_test(dir, "New-Alias gittest git", [ShellRcType::PowerShell]); }) .with_user_defaults(HashMap::from([( CompletionsOpenWhileTyping::storage_key().to_string(), true.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("One letter is too short to open the menu") .with_typed_characters(&["g"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert_eq!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::Closed, "InputSuggestions should be closed after inserting 'g'" ) }) }), ) .with_step( new_step_with_default_assertions("Two letters is long enough to open the menu") .with_typed_characters(&["i"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be open after inserting 'i'" ) }) }), ) .with_step( new_step_with_default_assertions("Type tte to narrow down to one result") .with_typed_characters(&["tte"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } ), "Tab completions are not open after typing 'tte'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { let first_item = view.items().first().expect("should be Some()").text(); async_assert_eq!( first_item, "gittest", "Expected first completion result to be 'gittest' but got '{first_item}'" ) }) }), ) .with_step( new_step_with_default_assertions( "Hitting tab should complete gittest and close the menu since it's the only entry with matching prefix", ) .with_keystrokes(&["tab"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { assert_eq!(view.buffer_text(ctx), "gittest".to_owned()); async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::Closed ), "InputSuggestions should be closed after completing 'gittest'" ) }) }), ) .with_step( new_step_with_default_assertions("Space should reopen the menu") .with_typed_characters(&[" "]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be open after adding space" ) }) }), ) .with_step( new_step_with_default_assertions( "Get close to an exact match between suggestion and buffer text", ) .with_typed_characters(&["commi"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "InputSuggestions should be typing 'commi'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { async_assert_eq!( view.items().first().expect("should be Some()").text(), "commit", "First completion result is not 'commit'" ) }) }), ) } pub fn test_completions_as_you_type_execute_on_enter() -> Builder { new_builder() .with_setup(|utils| { // Change dir to cargo test dir for auto cleanup. let dir = utils.test_dir(); let dir_string = dir .to_str() .expect("Should be able to convert test dir to str"); write_all_rc_files_for_test(&dir, format!("cd {dir_string}")); }) .with_user_defaults(HashMap::from([( CompletionsOpenWhileTyping::storage_key().to_string(), true.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir app apples && touch test_file test_file_2".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions( "Setup exact match between suggestion and buffer text with multiple suggestions", ) .with_typed_characters(&["cat test_file"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "Tab completions are not open after typing 'cat test_file'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { async_assert_eq!( view.items().first().expect("should be Some()").text(), "test_file", "First completion result is not test_file" ) }) }), ) .with_step( new_step_with_default_assertions( "Enter should execute on exact match when there are multiple suggestions", ) .with_keystrokes(&["enter"]) .add_assertion(assert_command_executed_for_single_terminal_in_tab( 0, "cat test_file".to_owned(), )) // need the active block to have received precmd before we can // get tab completions because we need a session id .add_assertion(assert_active_block_received_precmd(0, 0)), ) .with_step( new_step_with_default_assertions("Setup buffer to have dir with missing slash") .with_typed_characters(&["cd app"]) .add_assertion(|app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { println!("buffer text is {}", view.buffer_text(ctx)); async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::CompletionSuggestions { .. } ), "completions are not open after typing 'cd app'" ) }) }) .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { async_assert_eq!( view.items().first().expect("should be Some()").text(), "app/", "First completion result is not 'app/'" ) }) }), ) .with_step( new_step_with_default_assertions( "Enter should execute if suggestion only differs by buffer text by a slash", ) .with_keystrokes(&["enter"]) .add_assertion(assert_command_executed_for_single_terminal_in_tab( 0, "cd app".to_owned(), )), ) } pub fn test_alias_expansion_has_limit() -> Builder { new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#" alias a=b; alias b=c; alias c=d; alias d=e; alias e=f; alias f=git "#, [ShellRcType::Bash, ShellRcType::Zsh, ShellRcType::Fish], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Make sure there are multiple items in the working directory, // otherwise the tab completion menu will not appear (as there is // nothing for the user to choose between). .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir some_dir && touch some_file".to_owned(), ExpectedExitStatus::Success, (), )) .with_step(run_completer(0, "a ")) .with_step( new_step_with_default_assertions("ensure the suggestions are not for git") .add_assertion(|app, window_id| { let input_suggestions_view = single_input_suggestions_view_for_tab(app, window_id, 0); input_suggestions_view.read(app, |view, _| { async_assert!( !view.items().iter().any(|item| item.text() == "status"), "max alias expansion violated" ) }) }), ) } pub fn test_command_corrections() -> Builder { new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir -p foo/bar".to_owned(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "cd fo/br".to_owned(), ExpectedExitStatus::Failure, (), )) .with_step( new_step_with_default_assertions("Ensure the autosuggestion is present").add_assertion( |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |input_view, ctx| { let editor_view = input_view.editor().as_ref(ctx); async_assert_eq!( editor_view.current_autosuggestion_text(), Some("cd foo/bar"), "autosuggestion for command correction was not populated" ) }) }, ), ) } /// Tests that we can successfully start a shell from a directory that has been /// deleted. pub fn test_start_shell_in_deleted_directory() -> Builder { let initial_dir = PathBuf::from(cargo_target_tmpdir::get()).join("test_start_shell_in_deleted_directory"); new_builder() // TODO(CORE-2732): Flakey on Powershell (Linux) .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // Start the test in a fresh directory within the test temporary files // directory, to make the test more hermetic. .with_step(execute_command_for_single_terminal_in_tab( 0, format!("mkdir -p {}", initial_dir.display()), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("cd {}", initial_dir.display()), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "mkdir to-delete".to_owned(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "cd to-delete".to_owned(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Open a new tab using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) // Make sure the new tab opened in the to-delete subdirectory. .with_step(execute_command_for_single_terminal_in_tab( 1, format!( r#"test "$PWD" = "{}""#, initial_dir.join("to-delete").as_path().display() ), ExpectedExitStatus::Success, (), )) // Delete our current directory (the one the session started in). .with_step(execute_command_for_single_terminal_in_tab( 1, "rmdir ../to-delete".to_owned(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Open a new tab using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(2)) // Ensure a new tab opened from a session where the current and initial // directories don't exist can bootstrap successfully, and uses the // user's home directory as the initial directory. .with_step(execute_command_for_single_terminal_in_tab( 2, r#"test "$PWD" = "$HOME""#.to_owned(), ExpectedExitStatus::Success, (), )) } /// Tests that a new window will, by default, inherit the working directory from /// the active session in the previous window. pub fn test_new_window_inherits_previous_session_directory() -> Builder { let subdir_name = "subdir"; new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("mkdir {subdir_name}"), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("cd {subdir_name}"), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions( "Assert working directories match expectations (round 1)", ) .add_named_assertion("Check working directory for active session in each window", move |app, _| { app.window_ids() .into_iter() .fold(AssertionOutcome::Success, |prev, window_id| { if !matches!(prev, AssertionOutcome::Success) { return prev; } let views = app .views_of_type(window_id) .expect("Active window lacks a Workspace."); let workspace: &ViewHandle = views.first().expect("Window is missing Workspace view."); workspace.read(app, |workspace, ctx| { workspace .get_pane_group_view(0) .expect("Workspace has no tab view.") .read(ctx, |pane_group, ctx| { let session_path = pane_group.active_session_path(ctx); async_assert!( matches!(session_path.as_ref(), Some(path) if path.ends_with(subdir_name)), "Active session working directory should end with {subdir_name}; actually got {session_path:?}" ) }) }) }) }), ) .with_step( new_step_with_default_assertions("Open a new window") .with_action(|app, _, data| { let window_id = app.read(|ctx| { ctx.windows() .active_window() }); app.dispatch_global_action("root_view:open_new", ()); data.insert("first_window_id", window_id.expect("window id present")); }), ) .with_step( TestStep::new("Wait for window to open").add_assertion(move |app, _| { async_assert_eq!(app.window_ids().len(), 2, "Should have two windows open") }), ) .with_step( new_step_with_default_assertions( "Assert working directories match expectations (round 2)", ) .add_named_assertion_with_data_from_prior_step("Check active window is not initial window", move |app, _, data| { assert_eq!(app.window_ids().len(), 2, "Should have 2 windows open"); let Some(first_window_id) = data.get("first_window_id") else { return AssertionOutcome::failure("Expected window id to be passed from prior step".to_owned()); }; let active_window_id = app.read(|ctx| { ctx.windows() .active_window() }); assert!(active_window_id.is_some()); assert_ne!(Some(*first_window_id), active_window_id); AssertionOutcome::Success }) .add_named_assertion("Check working directory for session in active window", move |app, _| { let active_window_id = app.read(|ctx| { ctx.windows() .active_window() .expect("no active window") }); let views = app .views_of_type(active_window_id) .expect("Active window lacks a Workspace."); let workspace: &ViewHandle = views.first().expect("Window is missing Workspace view."); workspace.read(app, |workspace, ctx| { workspace .get_pane_group_view(0) .expect("Workspace has no tab view.") .read(ctx, |pane_group, ctx| { let session_path = pane_group.active_session_path(ctx); async_assert!( matches!(session_path.as_ref(), Some(path) if path.ends_with(subdir_name)), "Active session working directory should end with {subdir_name}; actually got {session_path:?}" ) }) }) }), ) } /// Checks that if the user configured a valid shell, we use it, and if not, /// we fall back to the default system shell. pub fn test_preferred_shell() -> Builder { let (starter, _) = current_shell_starter_and_version(); let temp_dir = PathBuf::from(cargo_target_tmpdir::get()).join("test_preferred_shell"); let shell_type = starter.shell_type(); let custom_shell = temp_dir.join(shell_type.name()); // Env vars in powershell need to be prefixed with `env:` // (ex: $env:SHELL), while sh-shells can simply access // env variables directly (ex: $SHELL) let var_prefix = match shell_type { ShellType::PowerShell => "env:", _ => "", }; new_builder() .with_user_defaults(HashMap::from([( StartupShellOverride::storage_key().to_owned(), serde_json::to_string(&custom_shell).expect("should serialize to JSON"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) // At first, the custom shell doesn't exist, so we fall back to the default shell. .with_step(execute_command_for_single_terminal_in_tab( 0, format!("echo ${var_prefix}SHELL"), ExpectedExitStatus::Success, starter.logical_shell_path().to_path_buf(), )) .with_step(execute_command_for_single_terminal_in_tab( 0, format!( "mkdir -p {} && ln -s ${var_prefix}SHELL {}", temp_dir.display(), custom_shell.display() ), ExpectedExitStatus::Success, (), )) // Now that the shell exists, a new tab should use it. .with_step( new_step_with_default_assertions("Open a new tab using cmd-t") .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, format!("echo ${var_prefix}SHELL"), ExpectedExitStatus::Success, custom_shell.display().to_string(), )) // After the shell is removed, new sessions should revert to the default shell. .with_step(execute_command_for_single_terminal_in_tab( 1, format!("rm -f {}", custom_shell.display()), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Open a new tab using cmd-t") .with_keystrokes(&[cmd_or_ctrl_shift("t")]), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(2)) .with_step(execute_command_for_single_terminal_in_tab( 2, format!("echo ${var_prefix}SHELL"), ExpectedExitStatus::Success, starter.logical_shell_path().display().to_string(), )) } /// Checks that the git branch is correct when outside of and inside of a git /// repo. pub fn test_git_prompt() -> Builder { let (starter, _) = current_shell_starter_and_version(); // Note that we can't use the OUT_DIR for the temp directory // here because that would put us in the warp repo. We need to // be in a place in the filesystem that's not already a git repo. new_builder() // TODO(CORE-2734): Unknown failure for Powershell .set_should_run_test(skip_if_powershell_core_2303) .use_tmp_filesystem_for_test_root_directory() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Git branch should be None").add_assertion( |app, window_id| validate_git_branch(None, 0, window_id, app), ), ) .with_step(execute_command_for_single_terminal_in_tab( 0, "git init -b main; git config user.email \"test@test.com\"; git config user.name \"Git TestUser\"".into(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "touch file".into(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "git add file; git commit -am \"commit\"".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Git branch should be main").add_assertion( |app, window_id| { validate_git_branch(Some("main".to_string()), 0, window_id, app) }, ), ) .with_step(execute_command_for_single_terminal_in_tab( 0, if starter.shell_type() == ShellType::Fish { "git checkout -q (git rev-parse HEAD) && echo (git rev-parse --short HEAD)" } else { "git checkout -q \"$(git rev-parse HEAD)\" && echo $(git rev-parse --short HEAD)" }.into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Git branch should be commit hash").add_assertion( |app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let branch_name = terminal_view.read(app, |view, _| { let model = view.model.lock(); let block = &model.block_list().last_non_hidden_block().expect("Must have visible blocks."); block.output_to_string() }); assert_ne!(branch_name, "HEAD"); validate_git_branch(Some(branch_name), 0, window_id, app) }, ), ) } pub fn test_terminal_announces_capabilities_to_shell() -> Builder { let (starter, _) = current_shell_starter_and_version(); // Env vars in powershell need to be prefixed with `env:` // (ex: $env:TERM), while sh-shells can simply access // env variables directly (ex: $TERM) let var_prefix = match starter.shell_type() { ShellType::PowerShell => "env:", _ => "", }; // Note that we can't use the OUT_DIR for the temp directory // here because that would put us in the warp repo. We need to // be in a place in the filesystem that's not already a git repo. new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("echo ${var_prefix}TERM"), ExpectedExitStatus::Success, "xterm-256color", )) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("echo ${var_prefix}TERM_PROGRAM"), ExpectedExitStatus::Success, "WarpTerminal", )) .with_step(execute_command_for_single_terminal_in_tab( 0, format!("echo ${var_prefix}COLORTERM"), ExpectedExitStatus::Success, "truecolor", )) } pub fn test_find_query_not_evaluated_on_terminal_mode_change() -> Builder { new_builder() // TODO(CORE-2732): Flakey on Powershell .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "seq 1000".to_string(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Open Find bar") .with_keystrokes(&[cmd_or_ctrl_shift("f")]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); let is_find_bar_focused = terminal_view.read(app, |view, ctx| view.is_find_bar_focused(ctx)); async_assert!( is_find_bar_open && is_find_bar_focused, "Expect the find bar to be open and focused", ) }), ) .with_step( new_step_with_default_assertions("Type into the find box") .with_typed_characters(&["42"]), ) .with_step( new_step_with_default_assertions("Dismiss the find box") .with_keystrokes(&["escape"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); let is_find_bar_open = terminal_view.read(app, |view, ctx| view.is_find_bar_open(ctx)); async_assert!(!is_find_bar_open, "Expect the find bar to be closed",) }), ) .with_step( // Don't perform default assertions here, as we expect to be in the // middle of an executing command. TestStep::new("Run git diff") .with_typed_characters(&["git diff"]) .with_keystrokes(&["enter"]) .set_post_step_pause(Duration::from_millis(100)) ) .with_step( new_step_with_default_assertions("Quit git diff") .with_typed_characters(&["q"]) ) .with_step( new_step_with_default_assertions("Assert scroll bar is at the bottom") .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _ctx| { async_assert_eq!(view.scroll_position(), ScrollPosition::FollowsBottomOfMostRecentBlock, "Expect the scroll position to be fixed to the bottom of the most recent block") }) }) ) } pub fn test_custom_open_completions_menu_binding() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Set custom keybinding for completions") .add_assertion(move |app, window_id| { // Set a custom keybinding for opening the completions menu to -1 app.update(|ctx| { ctx.set_custom_trigger( "input:open_completion_suggestions".to_owned(), Trigger::Keystrokes(vec![Keystroke { ctrl: true, key: "1".to_owned(), ..Default::default() }]), ); }); let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert_eq!( view.suggestions_mode_model().as_ref(ctx).mode(), &InputSuggestionsMode::Closed, "InputSuggestions should still be closed" ) }) }), ) .with_step( new_step_with_default_assertions("Attempt to trigger completions for ls") .with_typed_characters(&["ls", " "]) .with_keystrokes(&["ctrl-1"]) .set_timeout(Duration::from_secs(30)) .add_assertion(move |app, window_id| { let input_view = single_input_view_for_tab(app, window_id, 0); input_view.read(app, |view, ctx| { async_assert!( matches!( view.suggestions_mode_model().as_ref(ctx).mode(), InputSuggestionsMode::CompletionSuggestions { .. } ), "Tab completions are open after pressing ctrl-1" ) }) }), ) } /// This is a regression test for: /// https://linear.app/warpdotdev/issue/WAR-6095/panic-internal-error-entered-unreachable-code-handled-at-model-layer pub fn test_color_overrides_in_prompt_dont_crash() -> 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| { let dir = utils.test_dir(); write_rc_files_for_test( dir, r#"PS1=$(printf "hello> \x1b]4;1;rgb:0/0/0\x1b")"#, [ShellRcType::Zsh], ); }) .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, (), )) } pub fn test_copy_prompt_from_block_honor_ps1_disabled() -> Builder { new_builder() .use_tmp_filesystem_for_test_root_directory() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, // cding into home gives us a predictable prompt "cd $HOME".into(), ExpectedExitStatus::Success, (), )) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Select last block") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_steps(open_context_menu_for_selected_block()) .with_step( new_step_with_default_assertions("Copy prompt copies to clipboard properly") .with_click_on_saved_position("Copy prompt") .add_assertion(assert_clipboard_contains_string("~".into())), ) } pub fn test_copy_prompt_from_block_honor_ps1_enabled() -> Builder { let prompt_text = "this is my custom prompt"; new_builder() // TODO(CORE-2732): Flakey on linux .set_should_run_test(skip_if_powershell_core_2303) .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, format!(r#"export PS1="{prompt_text}""#), [ShellRcType::Bash, ShellRcType::Zsh], ); write_rc_files_for_test( &dir, format!( r#" function fish_prompt echo -n "{prompt_text}" end "# ), [ShellRcType::Fish], ); write_rc_files_for_test( &dir, format!( r#" function prompt {{ "{prompt_text}" }} "# ), [ShellRcType::PowerShell], ) }) .with_step( wait_until_bootstrapped_single_pane_for_tab(0).add_assertion(move |app, _window_id| { let input = single_input_view_for_tab(app, _window_id, 0); let input_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).0); async_assert_eq!(input_text, prompt_text) }), ) .with_step(execute_command_for_single_terminal_in_tab( 0, "ls".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Select last block") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_steps(open_context_menu_for_selected_block()) .with_step( new_step_with_default_assertions("Copy prompt copies to clipboard properly") .with_right_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("prompt_area_{}", input.id()) }) .with_click_on_saved_position("Copy prompt") .add_assertion(assert_clipboard_contains_string(String::from(prompt_text))), ) } pub fn test_copy_block_command_and_output_honor_ps1_disabled() -> Builder { let command = "echo WARP_COPY_E2E_OUTPUT"; new_builder() .use_tmp_filesystem_for_test_root_directory() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, command.into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Select last block") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_steps(open_context_menu_for_selected_block()) .with_step( new_step_with_default_assertions("Copy block copies command and output") .with_click_on_saved_position("Copy") .add_assertion(assert_clipboard_contains_string(format!( "{command}\nWARP_COPY_E2E_OUTPUT" ))), ) } pub fn test_copy_block_command_and_output_honor_ps1_enabled() -> Builder { let prompt_text = "this is my custom prompt"; let command = "echo WARP_PS1_COPY_E2E_OUTPUT"; new_builder() // TODO(CORE-2732): Flakey on linux .set_should_run_test(skip_if_powershell_core_2303) .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, format!(r#"export PS1="{prompt_text}""#), [ShellRcType::Bash, ShellRcType::Zsh], ); write_rc_files_for_test( &dir, format!( r#" function fish_prompt echo -n "{prompt_text}" end "# ), [ShellRcType::Fish], ); write_rc_files_for_test( &dir, format!( r#" function prompt {{ "{prompt_text}" }} "# ), [ShellRcType::PowerShell], ) }) .with_step( wait_until_bootstrapped_single_pane_for_tab(0).add_assertion(move |app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); let input_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).0); async_assert_eq!(input_text, prompt_text) }), ) .with_step(execute_command_for_single_terminal_in_tab( 0, command.into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Select last block") .with_keystrokes(&["cmdorctrl-up"]) .add_assertion(assert_selected_block_index_is_last_renderable()), ) .with_steps(open_context_menu_for_selected_block()) .with_step( new_step_with_default_assertions("Copy block includes PS1 prompt, command, and output") .with_click_on_saved_position("Copy") .add_assertion(assert_clipboard_contains_string(format!( "{prompt_text}{command}\nWARP_PS1_COPY_E2E_OUTPUT" ))), ) } pub fn test_copy_prompt_from_input_honor_ps1_disabled() -> Builder { new_builder() .use_tmp_filesystem_for_test_root_directory() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, // cding into home gives us a predictable prompt "cd $HOME".into(), ExpectedExitStatus::Success, (), )) .with_step( new_step_with_default_assertions("Copy prompt copies to clipboard properly") .with_right_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("prompt_area_{}", input.id()) }) .with_click_on_saved_position("Copy prompt") .add_assertion(assert_clipboard_contains_string("~".into())), ) } pub fn test_warp_prompt_unsets_zsh_rprompt() -> Builder { new_builder() .set_should_run_test(|| { let (starter, _) = current_shell_starter_and_version(); starter.shell_type() == shell::ShellType::Zsh }) .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), false.to_string(), )])) .with_setup(|utils| { let dir = utils.test_dir(); write_rc_files_for_test(&dir, r#"export RPROMPT="right prompt""#, [ShellRcType::Zsh]); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_command_for_single_terminal_in_tab( 0, "print -r -- ${+RPROMPT}".into(), ExpectedExitStatus::Success, ExactLine::from("0"), )) } pub fn test_copy_prompt_from_input_honor_ps1_enabled() -> Builder { let prompt_text = "this is my custom prompt"; new_builder() .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, format!(r#"export PS1="{prompt_text}""#), [ShellRcType::Bash, ShellRcType::Zsh], ); write_rc_files_for_test( &dir, format!( r#" function fish_prompt echo -n "{prompt_text}" end "# ), [ShellRcType::Fish], ); write_rc_files_for_test( &dir, format!( r#" function prompt {{ "{prompt_text}" }} "# ), [ShellRcType::PowerShell], ) }) .with_step( wait_until_bootstrapped_single_pane_for_tab(0).add_assertion(move |app, _window_id| { let input = single_input_view_for_tab(app, _window_id, 0); let input_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).0); async_assert_eq!(input_text, prompt_text) }), ) .with_step( new_step_with_default_assertions("Copy prompt copies to clipboard properly") .with_right_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("prompt_area_{}", input.id()) }) .with_click_on_saved_position("Copy prompt") .add_assertion(assert_clipboard_contains_string(String::from(prompt_text))), ) } pub fn test_copy_rprompt_from_input_honor_ps1_enabled() -> Builder { let rprompt_text = "right prompt"; new_builder() .set_should_run_test(|| { // Only run this one on zsh and fish let (starter, _) = current_shell_starter_and_version(); matches!( starter.shell_type(), shell::ShellType::Zsh | shell::ShellType::Fish ) }) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, format!(r#"export RPROMPT="{rprompt_text}""#), [ShellRcType::Zsh], ); write_rc_files_for_test( &dir, format!( r#" function fish_prompt echo -n "left prompt" end function fish_right_prompt echo -n "{rprompt_text}" end "# ), [ShellRcType::Fish], ); }) .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Ensure toggle_ps1 is on, rprompt shown in input") .add_assertion(move |app, _window_id| { app.read(|ctx| async_assert!(*SessionSettings::as_ref(ctx).honor_ps1.value())) }) .add_assertion(move |app, _window_id| { let input = single_input_view_for_tab(app, _window_id, 0); let input_text = input.read(app, |input, ctx| { input .prompt_and_rprompt_text(ctx) .1 .expect("rprompt should exist!") }); async_assert_eq!(input_text, rprompt_text) }), ) .with_step( new_step_with_default_assertions("Copy rprompt copies to clipboard properly") .with_right_click_on_saved_position_fn(|app, window_id| { let input = single_input_view_for_tab(app, window_id, 0); format!("rprompt_area_{}", input.id()) }) .with_keystrokes(&["down", "down", "enter"]) // Copy Right Prompt should be second option in context menu .add_assertion(assert_clipboard_contains_string(String::from(rprompt_text))), ) } pub fn test_rprompt_doesnt_show_when_not_enough_space() -> Builder { let prompt_text = "this is my custom prompt which is very very very very very very very very very very very long"; let rprompt_text = "this is my custom right prompt which is very very very very very very very very very very very long"; new_builder() .with_user_defaults(HashMap::from([( HonorPS1::storage_key().to_owned(), true.to_string(), )])) .set_should_run_test(|| { // Only run this one on zsh and fish let (starter, _) = current_shell_starter_and_version(); matches!( starter.shell_type(), shell::ShellType::Zsh | shell::ShellType::Fish ) }) .with_setup(move |utils| { let dir = utils.test_dir(); write_rc_files_for_test( &dir, format!(r#"export PS1="{prompt_text}" \n export RPROMPT="{rprompt_text}""#), [ShellRcType::Zsh], ); write_rc_files_for_test( &dir, format!( r#" function fish_prompt echo -n "{prompt_text}" end function fish_right_prompt echo -n "{rprompt_text}" end "# ), [ShellRcType::Fish], ); }) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions( "Switch to honor ps1 mode, prompt should exist, rprompt should not", ) .add_assertion(move |app, _window_id| { let input = single_input_view_for_tab(app, _window_id, 0); let input_prompt_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).0); async_assert_eq!(input_prompt_text, prompt_text) }) .add_assertion(move |app, _window_id| { let input = single_input_view_for_tab(app, _window_id, 0); let input_rprompt_text = input.read(app, |input, ctx| input.prompt_and_rprompt_text(ctx).1); async_assert_eq!(input_rprompt_text, None) }), ) } /// When in REPLs like irb or ipython, we want to make sure well-known word navigation keybindings /// get converted to the right control characters to move the cursor. This also applies to /// subshells which are not bootstrapped. pub fn test_block_cursor_navigation_using_escape_codes() -> Builder { let (starter, _) = current_shell_starter_and_version(); // On Linux, bash will overwrite an inherited PS1 variable with its choice // of default value. To work around this, we also set PROMPT_COMMAND // (which doesn't get clobbered) to set the PS1 variable, ensuring it has // the expected value after shell startup. let bash_command = match starter.shell_type() { ShellType::PowerShell => { r#"$env:BASH_SILENCE_DEPRECATION_WARNING=1; $env:PS1='> '; $env:PROMPT_COMMAND='export PS1="> "'; /bin/bash"# } _ => { r#"BASH_SILENCE_DEPRECATION_WARNING=1 PS1='> ' PROMPT_COMMAND='export PS1="> "' /bin/bash"# } }; new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Execute a REPL, but without bootstrapping a subshell") // The default bash prompt shows the version number, which we don't want polluting // the test so we overwrite PS1. Additionally, set the // `BASH_SILENCE_DEPRECATION_WARNING` env var so we don't a message on MacOS telling // us that this version of bash is deprecated. .with_typed_characters(&[bash_command]) .with_keystrokes(&["enter"]) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(true, 0), ) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> ", 0, )), ) .with_step( TestStep::new("Verify left word navigation") .with_typed_characters(&["echo hello world"]) .with_keystrokes(&["alt-left", "alt-left", "backspace"]) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> echohello world", 0, )), ) .with_step( TestStep::new("Verify right word navigation") .with_keystrokes(&["alt-right", "backspace"]) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> echohell world", 0, )), ) .with_step( TestStep::new("Verify beginning of line navigation") .with_keystrokes(&["right", "right", "right", "home", "delete"]) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> chohell world", 0, )), ) .with_step( TestStep::new("Verify end of line navigation") .with_keystrokes(&["end", "backspace"]) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( // We use a regex here to ignore trailing whitespace, which // can be present due to how backspace is typically implemented // (write a space over the cell). There is differing behavior // here between Linux and macOS, hence the regex. regex::Regex::new("> chohell worl *").expect("regex should compile"), 0, )), ) } /// Similar to above, we want to make sure deleting words and lines works in REPLs and shells pub fn test_block_bulk_deletion_using_escape_codes() -> Builder { let (starter, _) = current_shell_starter_and_version(); // On Linux, bash will overwrite an inherited PS1 variable with its choice // of default value. To work around this, we also set PROMPT_COMMAND // (which doesn't get clobbered) to set the PS1 variable, ensuring it has // the expected value after shell startup. let bash_command = match starter.shell_type() { ShellType::PowerShell => { r#"$env:BASH_SILENCE_DEPRECATION_WARNING=1; $env:PS1='> '; $env:PROMPT_COMMAND='export PS1="> "'; /bin/bash"# } _ => { r#"BASH_SILENCE_DEPRECATION_WARNING=1 PS1='> ' PROMPT_COMMAND='export PS1="> "' /bin/bash"# } }; new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( TestStep::new("Execute a REPL, but without bootstrapping a subshell") // The default bash prompt shows the version number, which we don't want polluting // the test so we overwrite PS1. Additionally, set the // `BASH_SILENCE_DEPRECATION_WARNING` env var so we don't a message on MacOS telling // us that this version of bash is deprecated. .with_typed_characters(&[bash_command]) .with_keystrokes(&["enter"]) .add_assertion( assert_long_running_block_executing_for_single_terminal_in_tab(true, 0), ) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> ", 0, )), ) .with_step( TestStep::new("Verify delete word") .with_typed_characters(&["echo hello world"]) .with_per_platform_keystroke(PerPlatformKeystroke { mac: "alt-backspace", linux_and_windows: "ctrl-backspace", }) .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> echo hello ", 0, )), ) .with_steps( open_command_palette_and_run_action("Delete to line start within an executing command") .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> ", 0, )), ) .with_step( TestStep::new("Verify delete to end") .with_typed_characters(&["echo hello world"]) .with_keystrokes(&["home", "right"]), ) .with_steps( open_command_palette_and_run_action("Delete to line end within an executing command") .add_assertion(assert_active_block_output_for_single_terminal_in_tab( "> e", 0, )), ) } /// Tests that any keydowns that would trigger an escape sequence to be written to a running program /// are only sent if the terminal is the focused terminal. pub fn test_escape_sequences_sent_to_focused_terminal() -> Builder { new_builder() // TODO(CORE-2857) There is some flakiness with long-running commands exiting. .set_should_run_test(skip_if_powershell_core_2303) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(TestStep::new("Create a new session").with_keystrokes(&[cmd_or_ctrl_shift("d")])) .with_step( wait_until_bootstrapped_pane(0 /* tab_index */, 1 /* pane_index */) .set_timeout(Duration::from_secs(10)) .add_assertion(assert_terminal_bootstrapped( 0, /* tab_index */ 1, /* pane_index */ )), ) .with_steps( open_command_palette_and_run_action("Activate Previous Pane").add_named_assertion( "Assert first pane is focused", assert_focused_pane_index(0 /* tab_index */, 0 /* pane_index */), ), ) .with_step( TestStep::new("Execute sleep") .with_typed_characters(&["sleep 999"]) .with_keystrokes(&["enter"]) .add_assertion(assert_long_running_block_executing( true, 0, /* tab_index */ 0, /* pane_index */ )), ) .with_steps( open_command_palette_and_run_action("Activate Next Pane").add_named_assertion( "Assert second pane is focused", assert_focused_pane_index(0 /* tab_index */, 1 /* pane_index */), ), ) .with_step( execute_long_running_command_for_pane( 0, /* tab_index */ 1, /* pane_index */ "vim", ) .add_assertion(assert_alt_grid_active( 0, /* tab_index */ 1, /* pane_index */ true, )), ) .with_step( TestStep::new("Ensure the cursor moves to the left in vim by pressing `shift-left`") .with_typed_characters(&["a"]) .with_keystrokes(&["shift-left"]) .with_typed_characters(&["bar"]) // The running command in the first pane should be unchanged. .add_assertion(assert_active_block_output( "", 0, /* tab_index */ 0, /* pane_index */ )) .add_assertion(assert_alt_screen_output( ExactLine::from("bar"), 0, /* tab_index */ 1, /* pane_index */ )), ) } // When SGR_MOUSE is set, that means we'll only intercept the mouse event // if mouse reporting is disabled. Since it's enabled in this case, that means // we shouldn't intercept (vim should handle the right-click) so we don't want to // open the context-menu on right-click in this case. pub fn test_alt_screen_context_menu_with_sgr_with_mouse_reporting() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([( MouseReportingEnabled::storage_key().to_owned(), serde_json::to_string(&true).expect("bool should convert to string"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)); let exit_step = TestStep::new("Close vim") .with_keystrokes(&["escape"]) .with_typed_characters(&[":q!"]) .with_keystrokes(&["enter"]); let steps = run_alt_grid_program( "vim", 0, 0, exit_step, vec![ TestStep::new("Turn SGR_MOUSE on for vim") .with_typed_characters(&[":set mouse=a"]) .with_keystrokes(&["enter"]) .add_assertion(assert_model_term_mode(TermMode::SGR_MOUSE, true)), TestStep::new("Right click to try to open context menu but it shouldn't open") .with_event(Event::RightMouseDown { position: Vector2F::new(500., 100.), cmd: false, shift: false, click_count: 1, }) .add_assertion(assert_context_menu_is_open(false)), ], ); builder = builder.with_steps(steps); builder } // When SGR_MOUSE is set, that means we'll only intercept the mouse event // if mouse reporting is disabled. Thus, we want to open the context-menu // on right-click in this case. pub fn test_alt_screen_context_menu_with_sgr_without_mouse_reporting() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([( MouseReportingEnabled::storage_key().to_owned(), serde_json::to_string(&false).expect("bool should convert to string"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)); let exit_step = TestStep::new("Close vim") .with_keystrokes(&["escape"]) .with_typed_characters(&[":q!"]) .with_keystrokes(&["enter"]); let steps = run_alt_grid_program( "vim", 0, 0, exit_step, vec![ TestStep::new("Turn SGR_MOUSE on for vim") .with_typed_characters(&[":set mouse=a"]) .with_keystrokes(&["enter"]) .add_assertion(assert_model_term_mode(TermMode::SGR_MOUSE, true)), TestStep::new("Right click to open context menu") .with_event(Event::RightMouseDown { position: Vector2F::new(400., 100.), cmd: false, shift: false, click_count: 1, }) .add_assertion(assert_context_menu_is_open(true)), TestStep::new("Close the menu with escape") .with_keystrokes(&["escape"]) .add_assertion(assert_context_menu_is_open(false)), ], ); builder = builder.with_steps(steps); builder } // When SGR_MOUSE is not set, we always intercept the mouse events // so we definitely want to open the context-menu on right-click. pub fn test_alt_screen_context_menu_without_sgr_with_mouse_reporting() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([( MouseReportingEnabled::storage_key().to_owned(), serde_json::to_string(&true).expect("bool should convert to string"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)); let exit_step = TestStep::new("Close vim") .with_keystrokes(&["escape"]) .with_typed_characters(&[":q!"]) .with_keystrokes(&["enter"]); let steps = run_alt_grid_program( "vim", 0, 0, exit_step, vec![ TestStep::new("Turn SGR_MOUSE off for vim") .with_typed_characters(&[":set mouse="]) .with_keystrokes(&["enter"]) .add_assertion(assert_model_term_mode(TermMode::SGR_MOUSE, false)), TestStep::new("Right click to open context menu") .with_event(Event::RightMouseDown { position: Vector2F::new(400., 100.), cmd: false, shift: false, click_count: 1, }) .add_assertion(assert_context_menu_is_open(true)), TestStep::new("Close the menu with escape") .with_keystrokes(&["escape"]) .add_assertion(assert_context_menu_is_open(false)), ], ); builder = builder.with_steps(steps); builder } // When SGR_MOUSE is not set, we always intercept the mouse events // so we definitely want to open the context-menu on right-click. pub fn test_alt_screen_context_menu_without_sgr_without_mouse_reporting() -> Builder { let mut builder = new_builder() .with_user_defaults(HashMap::from([( MouseReportingEnabled::storage_key().to_owned(), serde_json::to_string(&false).expect("bool should convert to string"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)); let exit_step = TestStep::new("Close vim") .with_keystrokes(&["escape"]) .with_typed_characters(&[":q!"]) .with_keystrokes(&["enter"]); let steps = run_alt_grid_program( "vim", 0, 0, exit_step, vec![ TestStep::new("Turn SGR_MOUSE off for vim") .with_typed_characters(&[":set mouse="]) .with_keystrokes(&["enter"]) .add_assertion(assert_model_term_mode(TermMode::SGR_MOUSE, false)), TestStep::new("Right click to open context menu") .with_event(Event::RightMouseDown { position: Vector2F::new(400., 100.), cmd: false, shift: false, click_count: 1, }) .add_assertion(assert_context_menu_is_open(true)), TestStep::new("Close the menu with escape") .with_keystrokes(&["escape"]) .add_assertion(assert_context_menu_is_open(false)), ], ); builder = builder.with_steps(steps); builder } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_pane_group_state_single_pane() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_python_interpreter_in_tab(0)) .with_step( new_step_with_default_assertions("exit the interpreter without errors") .with_keystrokes(&["ctrl-d"]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Normal)), ) .with_step( execute_command_for_single_terminal_in_tab( 0, "false".to_owned(), ExpectedExitStatus::Failure, (), ) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Errored)), ) .with_step( execute_command_for_single_terminal_in_tab( 0, "ls".to_owned(), ExpectedExitStatus::Success, (), ) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Normal)), ) .with_step(execute_python_interpreter_in_tab(0)) .with_step( new_step_with_default_assertions("exit the interpreter with an error status") .with_typed_characters(&["exit(1)"]) .with_keystrokes(&["enter"]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Errored)), ) } // TODO(CORE-2721): Block count / index Failed b/c of in-band generators pub fn test_pane_group_state_multi_pane() -> Builder { new_builder() .with_step( new_step_with_default_assertions("create 2 additional panes") .with_keystrokes(&[cmd_or_ctrl_shift("d")]) .with_keystrokes(&[cmd_or_ctrl_shift("d")]), ) .with_step(wait_until_bootstrapped_pane(0, 0)) .with_step(wait_until_bootstrapped_pane(0, 1)) .with_step(wait_until_bootstrapped_pane(0, 2)) .with_steps( open_command_palette_and_run_action("Activate Previous Pane") .add_named_assertion("Assert pane 1 is focused", assert_focused_pane_index(0, 1)), ) .with_steps( open_command_palette_and_run_action("Activate Previous Pane") .add_named_assertion("Assert pane 0 is focused", assert_focused_pane_index(0, 0)), ) .with_step( TestStep::new("(pane 0) run long-running command") .with_typed_characters(&["python3"]) .with_keystrokes(&["enter"]) .add_assertion(assert_pane_group_has_state( 0, TerminalViewState::LongRunning, )), ) .with_steps( open_command_palette_and_run_action("Activate Next Pane") .add_named_assertion("Assert pane 1 is focused", assert_focused_pane_index(0, 1)), ) .with_step( TestStep::new("(pane 1) run another long-running command") .with_typed_characters(&["python3"]) .with_keystrokes(&["enter"]) .add_assertion(assert_pane_group_has_state( 0, TerminalViewState::LongRunning, )), ) .with_steps( open_command_palette_and_run_action("Activate Next Pane") .add_named_assertion("Assert pane 2 is focused", assert_focused_pane_index(0, 2)), ) .with_step( TestStep::new("(pane 2) run command that errors") .with_typed_characters(&["false"]) .with_keystrokes(&["enter"]) // the most recent overall state change is now that we're in an error state .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Errored)), ) .with_step( TestStep::new("(pane 2) clear the error by running normal command") .with_typed_characters(&["true"]) .with_keystrokes(&["enter"]) // pane 2 is normal, so we return to our next most-recent state, which is long running .add_assertion(assert_pane_group_has_state( 0, TerminalViewState::LongRunning, )), ) .with_steps( open_command_palette_and_run_action("Activate Previous Pane") .add_named_assertion("Assert pane 1 is focused", assert_focused_pane_index(0, 1)), ) .with_step( TestStep::new( "(pane 1) terminate command with non-zero status, and verify error state", ) .with_typed_characters(&["exit(1)"]) .with_keystrokes(&["enter"]) // once pane 1 has finished & errored, our most recent state is errored .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Errored)), ) .with_step( TestStep::new("(pane 1) clear the error by running normal command") .with_typed_characters(&["ls"]) .with_keystrokes(&["enter"]) // pane 1 is normal, so we return to our next most-recent state, which is long running .add_assertion(assert_pane_group_has_state( 0, TerminalViewState::LongRunning, )), ) .with_steps( open_command_palette_and_run_action("Activate Previous Pane") .add_named_assertion("Assert pane 0 is focused", assert_focused_pane_index(0, 0)), ) .with_step( new_step_with_default_assertions_for_pane( "(pane 0) terminate command with no errors, returning back to normal state", 0, 2, ) .with_keystrokes(&["ctrl-d"]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Normal)), ) } pub fn test_pane_group_state_close_pane() -> Builder { new_builder() .with_step( new_step_with_default_assertions("create 1 additional pane") .with_keystrokes(&[cmd_or_ctrl_shift("d")]), ) .with_step(wait_until_bootstrapped_pane(0, 0)) .with_step(wait_until_bootstrapped_pane(0, 1)) .with_step( TestStep::new("(pane 2) run long-running command") .with_typed_characters(&["python3"]) .with_keystrokes(&["enter"]) .add_assertion(assert_pane_group_has_state( 0, TerminalViewState::LongRunning, )), ) .with_step( new_step_with_default_assertions("(pane 2) close pane") .with_keystrokes(&[cmd_or_ctrl_shift("w")]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Normal)), ) } pub fn test_pane_group_state_clear_blocks() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("run a non existent command") .with_typed_characters(&["dsfsdjfpdsojfpdsofj"]) .with_keystrokes(&["enter"]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Errored)), ) .with_step( new_step_with_default_assertions("clear the pane") .with_keystrokes(&[cmd_or_ctrl_shift("k")]) .add_assertion(assert_pane_group_has_state(0, TerminalViewState::Normal)), ) } /// Create a small window and enough terminal panes inside it so that a new pane would normally have /// a narrow width. Check that the Agent Mode pane is wide enough regardless. pub fn test_agent_mode_pane_minimum_size() -> Builder { const WINDOW_ID_KEY: &str = "small_window_id"; new_builder() .with_step(set_window_custom_size(40, 120)) .with_step(add_and_save_window(WINDOW_ID_KEY)) .with_step( new_step_with_default_assertions("Check the new window size") .add_named_assertion_with_data_from_prior_step( "Validate window size", move |app, _, step_data_map| { let window_id = step_data_map .get(WINDOW_ID_KEY) .expect("Window ID for new window should exist"); let size = app .window_bounds(window_id) .expect("Window should exist") .size(); // This doesn't correspond clearly to the given rows and columns due to line // height and padding. There's also some platform-specific variance and room // for floating-point error. assert_approx_eq!(f32, size.x(), 992., epsilon = 2.); assert_approx_eq!(f32, size.y(), 644., epsilon = 2.); AssertionOutcome::Success }, ), ) .with_step( new_step_with_default_assertions("Create a new empty pane") .with_keystrokes(&[cmd_or_ctrl_shift("d")]), ) .with_step( new_step_with_default_assertions("Create an Agent Mode pane and check its width") .with_action(move |app, _, step_data_map| { let window_id = step_data_map .get(WINDOW_ID_KEY) .expect("Window ID for new window should exist"); let workspace_view_id = workspace_view(app, *window_id).id(); app.dispatch_typed_action( *window_id, &[workspace_view_id], &WorkspaceAction::NewPaneInAgentMode { entrypoint: AgentModeEntrypoint::TabBar, zero_state_prompt_suggestion_type: None, }, ); }) .add_named_assertion_with_data_from_prior_step( "Check Agent Mode pane width", |app, _, step_data_map| { let window_id = step_data_map .get(WINDOW_ID_KEY) .expect("Window ID for new window should exist"); let pane_group = pane_group_view(app, *window_id, 0); pane_group.read(app, |view, app| { let Some(agent_mode_pane) = view.terminal_view_at_pane_index(2, app) else { return AssertionOutcome::failure( "no terminal pane at pane_index 2".to_owned(), ); }; let pane_width = agent_mode_pane.as_ref(app).size_info().pane_size_px().x(); // Approx equality to handle pane borders, etc. assert_approx_eq!( f32, pane_width - AGENT_MODE_PANE_DEFAULT_MINIMUM_WIDTH, 0., epsilon = 8. ); AssertionOutcome::Success }) }, ), ) } // cheating a little bit in this test; it's hard to tell if the create folder dialog is open from // the workspace view, but we DO force warp drive open to show the dialog, so we can look for that pub fn test_create_folder_from_command_palette() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(join_a_workspace()) .with_step(go_offline()) .with_steps( open_command_palette_and_run_action("Create a New Team Folder") .add_assertion(assert_warp_drive_is_closed()), ) .with_steps( open_command_palette_and_run_action("Create a New Personal Folder") .add_assertion(assert_warp_drive_is_closed()), ) .with_step(go_online()) .with_steps( open_command_palette_and_run_action("Create a New Team Folder") .add_assertion(assert_warp_drive_is_open()), ) .with_steps( open_command_palette_and_run_action("Create a New Personal Folder") .add_assertion(assert_warp_drive_is_open()), ) } pub fn test_tab_behavior_setting() -> Builder { let completions_binding_name = "input:open_completion_suggestions"; let autosuggestions_binding_name = "editor_view:insert_autosuggestion"; let expected_completion_binding_name = if OperatingSystem::get().is_mac() { "⌃Space" } else { "Ctrl Space" }; new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(toggle_setting(SettingsAction::FeaturesPageToggle( FeaturesPageAction::SetTabBehavior(TabBehavior::Autosuggestions), ))) .with_step(assert_binding_display_string( autosuggestions_binding_name, Some("Tab"), )) .with_step(assert_binding_display_string( completions_binding_name, Some(expected_completion_binding_name), )) .with_step(toggle_setting(SettingsAction::FeaturesPageToggle( FeaturesPageAction::SetTabBehavior(TabBehavior::Completions), ))) .with_step(assert_binding_display_string( autosuggestions_binding_name, None, )) .with_step(assert_binding_display_string( completions_binding_name, Some("Tab"), )) } pub fn test_context_chips_prompt_at_bootstrap() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_user_defaults(HashMap::from([ (HonorPS1::storage_key().to_owned(), false.to_string()), (String::from("SavedPrompt"), String::from("Default")), ])) .with_step( new_step_with_default_assertions("Check Warp prompt") .add_assertion(assert_working_dir_is_present(0)), ) } pub fn test_pass_control_sequences_to_long_running_block() -> Builder { new_builder() .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step(execute_long_running_command_for_pane(0, 0, "cat -v")) .with_step( TestStep::new("Type F2 into cat -v") .with_keystrokes(&["f2"]) .add_assertion(|app, window_id| { let terminal_view = single_terminal_view_for_tab(app, window_id, 0); terminal_view.read(app, |view, _| { // `cat -v` writes the control sequences as visible characters. let output = view .model .lock() .block_list() .active_block() .output_to_string(); async_assert_eq!( output, String::from("^[OQ"), "Control sequences should be passed through to the long-running command" ) }) }), ) } /// Test that undo close stack cleanup works safely when a window is closed /// before the grace period expires. This reproduces the specific bug scenario: /// 1. Open a new window /// 2. In that window open a new tab /// 3. Close that tab (adds it to undo close stack) /// 4. Close the window before grace period expires /// 5. Verify cleanup handles missing window gracefully pub fn test_undo_close_stack_timeout_cleanup() -> Builder { FeatureFlag::UndoClosedPanes.set_enabled(true); new_builder() // This test is Mac-only due to differences in window management on Linux .set_should_run_test(|| cfg!(target_os = "macos")) // Set a 5-second grace period to give time to close the window before it expires .with_user_defaults(HashMap::from([( "UndoCloseGracePeriod".to_owned(), serde_json::to_string(&Duration::from_secs(5)) .expect("Duration should serialize to JSON"), )])) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Assert we have only 1 window open at start") .add_assertion(move |app, _| async_assert_eq!(app.window_ids().len(), 1)), ) .with_step(add_and_save_window("new_window_id")) .with_step( new_step_with_default_assertions("Assert we now have 2 windows") .add_assertion(move |app, _| async_assert_eq!(app.window_ids().len(), 2)), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) .with_step( new_step_with_default_assertions("Add a new tab in the new window") .with_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID) .add_assertion(assert_tab_count(2)), ) .with_step(wait_until_bootstrapped_single_pane_for_tab(1)) .with_step( new_step_with_default_assertions("Close the tab to trigger undo close stack") .with_hover_over_saved_position("close_tab_button:1") .with_click_on_saved_position("close_tab_button:1") .add_assertion(assert_tab_count(1)), ) .with_step( TestStep::new("Close the new window") .with_action(|app, _, data_map| { let window_id = data_map .get("new_window_id") .expect("Expected window id to be in data map"); app.update(|ctx| { WindowManager::as_ref(ctx) .close_window(*window_id, TerminationMode::ForceTerminate); }); // Switch back to the original window (WindowId(0)) let original_window = app.window_ids()[0]; app.update(|ctx| { WindowManager::as_ref(ctx).show_window_and_focus_app(original_window); }); }) .add_assertion(|app, _| { // Verify we now have only 1 window remaining async_assert_eq!(app.window_ids().len(), 1) }), ) .with_step( TestStep::new("Wait for undo close grace period to expire and trigger cleanup") .set_timeout(Duration::from_secs(8)) .with_action(|_app, _, _data| { // Wait longer than the grace period to ensure cleanup is triggered std::thread::sleep(Duration::from_secs(6)); }) // After waiting, verify the application is still stable .add_assertion(|app, _| { // Simple stability check - ensure we still have 1 window async_assert_eq!(app.window_ids().len(), 1) }), ) }