Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+450
View File
@@ -0,0 +1,450 @@
use std::{collections::HashMap, env};
use anyhow::Result;
use clap::Parser;
use integration::test::*;
use integration::Builder;
use warp_cli::WorkerCommand;
use warp_core::channel::{Channel, ChannelConfig, ChannelState, OzConfig, WarpServerConfig};
use warp_core::AppId;
/// The Warp integration test runner.
#[derive(Debug, Default, Parser, Clone)]
#[command(name = "warp-integration-test")]
#[clap(args_conflicts_with_subcommands = true)]
pub struct Args {
#[command(subcommand)]
command: Option<WorkerCommand>,
/// Integration test name.
#[arg(value_name = "INTEGRATION_TEST_NAME", required = true)]
// This is an Option<String> because it's not set if a subcommand is requested.
integration_test_name: Option<String>,
}
pub fn main() -> Result<()> {
ChannelState::set(ChannelState::new(
Channel::Integration,
ChannelConfig {
app_id: AppId::new(
"dev",
"warp",
if cfg!(target_os = "macos") {
"Warp-Integration"
} else {
"WarpIntegration"
},
),
logfile_name: "warp_integration.log".into(),
server_config: WarpServerConfig {
firebase_auth_api_key: "".into(),
// Use an IP in the IANA testing range, with the TCP discard port, to
// black-hole server traffic.
server_root_url: "http://192.0.2.0:9".into(),
rtc_server_url: "ws://192.0.2.0:9/graphql/v2".into(),
session_sharing_server_url: None,
},
oz_config: OzConfig {
// Use an IP in the IANA testing range, with the TCP discard port, to
// black-hole server traffic.
oz_root_url: "http://192.0.2.0:9".into(),
workload_audience_url: None,
},
telemetry_config: None,
crash_reporting_config: None,
autoupdate_config: None,
mcp_static_config: None,
},
));
let args = Args::parse();
if let Some(command) = &args.command {
match command {
#[cfg(unix)]
WorkerCommand::TerminalServer(args) => {
// If we were asked to run as a terminal server (as opposed to the main
// GUI application), do so. This must occur before init_logging, as the
// terminal server sets up its own logger, and attempting to set a second
// logger leads to a panic.
warp::terminal::local_tty::server::run_terminal_server(args);
return Ok(());
}
// This is a catch-all to handle the plugin host, which the integration test crate doesn't have a feature flag for.
#[allow(unreachable_patterns)]
other => panic!("Worker not supported in integration tests: {other:?}"),
}
}
let tests = register_tests();
let test_name = args
.integration_test_name
.as_deref()
.expect("Integration test name is required");
println!("Running integration test: {test_name}");
let Some(builder) = tests.get(test_name).map(|func| func()) else {
panic!("test not found for args: {:#?}", env::args());
};
#[cfg_attr(not(unix), allow(unused_variables))]
let driver = builder.build(test_name, true);
// Before actually running the test, make sure we won't accidentally stop
// on any of the real user's configuration or rcfiles.
cfg_if::cfg_if! {
if #[cfg(unix)] {
let home =
std::env::var("HOME").expect("Should have a value for the HOME environment variable");
let original_home = std::env::var("ORIGINAL_HOME").expect(
"Integration test binary should have set an ORIGINAL_HOME environment variable",
);
assert_ne!(home, original_home, "HOME should not be the same as ORIGINAL_HOME!");
} else {
unimplemented!("Need to add support for hermetic integration tests for the current platform!");
}
}
#[cfg_attr(not(unix), allow(unreachable_code))]
warp::run_integration_test(driver)
}
/// Type of a function that produces an integration test builder.
type BoxedBuilderFn = Box<dyn Fn() -> Builder>;
fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> {
let mut tests: HashMap<&str, BoxedBuilderFn> = HashMap::new();
// A tiny macro to simplify the act of registering a test. This avoids
// any inconsistencies between the test function name and the key in the
// map, and makes it easier to change how we register the tests (if we
// decide to do so in the future).
macro_rules! register_test {
($name:ident) => {
tests.insert(stringify!($name), Box::new(|| $name()));
};
}
// Add new tests here
register_test!(test_single_command);
register_test!(test_add_and_close_session);
register_test!(test_add_many_sessions);
register_test!(test_ctrl_tab_session_switching);
register_test!(test_ctrl_d_eot);
register_test!(test_ctrl_d_exit);
register_test!(test_ctrl_d_handled_by_read_during_bootstrapping);
register_test!(test_ctrl_d_during_bootstrapping_exits_shell_upon_completion);
register_test!(test_hover_over_menu);
register_test!(test_zshrc_keypress);
register_test!(test_bootstrap_with_no_script_execution_block);
register_test!(test_instant_prompt_bootstrap);
register_test!(test_rc_files_only_sourced_once_during_bootstrapping);
register_test!(test_unescaped_prompt_bootstraps);
register_test!(test_detect_powerlevel10k);
register_test!(test_open_and_close_resource_center);
register_test!(test_block_based_snackbar_scroll_to_top);
register_test!(test_block_based_snackbar_small_window);
register_test!(test_block_based_snackbar_appears_for_running_command_input_at_bottom);
register_test!(test_block_based_snackbar_not_visible_for_pager_command_input_at_bottom);
register_test!(test_block_based_snackbar_appears_for_running_command_pinned_to_top);
register_test!(test_block_based_snackbar_not_visible_for_pager_command_pinned_to_top);
register_test!(test_block_based_snackbar_appears_for_running_command_waterfall_mode);
register_test!(test_block_based_snackbar_not_visible_pager_command_waterfall_mode);
register_test!(test_shell_reinitializing);
register_test!(test_exit_multiple_tabs);
register_test!(test_open_context_menu_and_execute_command);
register_test!(test_open_and_close_context_menu_with_keybinding);
register_test!(test_block_metadata_received);
register_test!(test_scroll_to_hidden_block_and_open_context_menu_with_keybinding);
register_test!(test_block_navigation);
register_test!(test_execute_multiple_cursor_command);
register_test!(test_undo_redo);
register_test!(test_add_windows_correct_position_and_cascade);
register_test!(test_typeahead);
register_test!(test_input_reporting_posix_shells);
register_test!(test_input_reporting_powershell);
register_test!(test_background_output);
register_test!(test_home_key_should_not_appear_in_input);
register_test!(test_change_font_size);
register_test!(test_long_running_block_height_updated);
register_test!(test_unnecessary_resizes);
register_test!(test_open_and_close_settings);
register_test!(test_suggestions_menu_positioning);
register_test!(test_open_and_close_theme_creator_modal);
register_test!(test_removing_tabs_out_of_order);
register_test!(test_ctrl_c);
register_test!(test_click_on_prompt_to_focus_input);
register_test!(test_text_input_on_block_list);
register_test!(test_text_input_on_block_list_while_composing);
register_test!(test_clear);
register_test!(test_waterfall_input);
register_test!(test_waterfall_input_text_selection);
register_test!(test_waterfall_input_scrolling);
register_test!(test_waterfall_input_after_command_execution);
register_test!(test_waterfall_input_alt_grid);
register_test!(test_find_within_block);
register_test!(test_case_sensitive_find);
register_test!(test_find_bar_autoselects_text);
register_test!(test_disabling_action_dispatching);
register_test!(test_session_restoration);
register_test!(test_restored_blocks_on_different_hosts);
register_test!(test_restore_snapshot_with_deleted_cwd);
register_test!(test_session_restoration_with_multiple_shells);
register_test!(test_restore_snapshot_with_background_output);
register_test!(test_restore_snapshot_with_notebooks);
register_test!(test_restore_snapshot_with_workflows);
register_test!(test_restore_snapshot_with_test_json_object);
register_test!(test_restore_snapshot_with_common_shareable_metadata_ids);
register_test!(test_restore_snapshot_with_markdown_file);
register_test!(test_restore_snapshot_with_code_file);
register_test!(test_restore_snapshot_with_settings_page);
register_test!(test_multi_block_selections);
register_test!(test_alias_guards_on_ps1_set);
register_test!(test_ps1_value_not_null_or_exit);
register_test!(test_custom_ps1_expansion_bash);
register_test!(test_completions_with_autocd);
register_test!(test_auto_title);
register_test!(test_warp_auto_title_disabled);
register_test!(test_warp_honors_user_title_bash);
register_test!(test_warp_honors_user_title_zsh);
register_test!(test_input_focused_after_executing_command);
register_test!(test_new_session_focuses_input);
register_test!(test_executable_completions);
register_test!(test_function_completions);
register_test!(test_builtin_completions);
register_test!(test_keyword_completions);
register_test!(test_with_launch_config);
register_test!(test_command_xray_hover);
register_test!(test_command_xray_for_partial_command);
register_test!(test_ctrl_r_multi_cursor);
register_test!(test_histcontrol_env_var);
register_test!(test_session_navigation_recency_change_tab);
register_test!(test_session_navigation_recency_navigate_to_tab);
register_test!(test_session_navigation_recency_click_on_window);
register_test!(test_session_navigation_recency_navigate_to_window);
register_test!(test_completions_as_you_type);
register_test!(test_completions_as_you_type_one_matching_entry_tab);
register_test!(test_completions_as_you_type_execute_on_enter);
register_test!(test_accepting_completion_inserts_space);
register_test!(test_create_session_with_split_pane_while_bootstrapping);
register_test!(test_create_session_with_new_tab_while_bootstrapping);
register_test!(test_add_theme_to_warp_config);
register_test!(test_palette_opens_when_theme_chooser_is_open);
#[cfg(target_os = "macos")]
register_test!(test_preview_config_dir_migration);
register_test!(test_launch_warp_with_theme_in_warp_config);
register_test!(test_add_launch_config_to_warp_config);
register_test!(test_add_workflows_to_warp_config);
register_test!(test_loading_project_workflows);
register_test!(test_cmd_enter);
register_test!(test_alias_expansion_has_limit);
register_test!(test_command_corrections);
register_test!(test_start_shell_in_deleted_directory);
register_test!(test_new_window_inherits_previous_session_directory);
register_test!(test_preferred_shell);
register_test!(test_git_prompt);
register_test!(test_terminal_announces_capabilities_to_shell);
register_test!(test_open_new_tab_with_specific_shell_from_new_session_menu);
register_test!(test_open_launch_config_from_add_tab_menu_legacy);
register_test!(test_open_launch_config_with_custom_size);
register_test!(test_launch_config_single_child_branch);
register_test!(test_open_launch_config_in_active_window);
register_test!(test_with_launch_config_with_active_tab_index);
register_test!(test_with_launch_config_with_active_pane);
register_test!(test_with_launch_config_with_no_active_pane);
register_test!(test_find_query_not_evaluated_on_terminal_mode_change);
register_test!(test_bash_bootstraps_with_prompt_command_array);
register_test!(test_bash_bootstraps_with_prompt_command_array_that_sets_ps1);
register_test!(test_zsh_bootstraps_with_nounset_option);
register_test!(test_legacy_ssh_into_bash);
register_test!(test_legacy_ssh_into_zsh);
register_test!(test_tmux_ssh_into_bash);
register_test!(test_tmux_ssh_into_zsh);
register_test!(test_ssh_into_fish);
register_test!(test_ssh_into_sh);
register_test!(test_ssh_into_ash);
register_test!(test_ssh_with_shell_override);
register_test!(test_custom_open_completions_menu_binding);
register_test!(test_color_overrides_in_prompt_dont_crash);
register_test!(test_copy_prompt_from_block_honor_ps1_disabled);
register_test!(test_copy_prompt_from_block_honor_ps1_enabled);
register_test!(test_copy_prompt_from_input_honor_ps1_disabled);
register_test!(test_copy_prompt_from_input_honor_ps1_enabled);
register_test!(test_copy_rprompt_from_input_honor_ps1_enabled);
register_test!(test_rprompt_doesnt_show_when_not_enough_space);
register_test!(test_block_cursor_navigation_using_escape_codes);
register_test!(test_block_bulk_deletion_using_escape_codes);
register_test!(test_escape_sequences_sent_to_focused_terminal);
register_test!(test_open_input_context_menu);
register_test!(test_copy_all_from_input_context_menu);
register_test!(test_cut_paste_from_input_context_menu);
register_test!(test_paste_and_type_characters_before_bootstrap);
register_test!(test_code_review_scroll_anchor_preserved_when_inserting_above);
register_test!(test_code_review_scroll_anchor_unchanged_when_inserting_below);
register_test!(test_code_review_scroll_preserved_second_file);
register_test!(test_code_review_scroll_preserved_deleted_range);
register_test!(test_code_review_scroll_preserved_header_range);
register_test!(test_code_review_scroll_preserved_footer_range);
register_test!(test_alt_screen_context_menu_with_sgr_with_mouse_reporting);
register_test!(test_alt_screen_context_menu_with_sgr_without_mouse_reporting);
register_test!(test_alt_screen_context_menu_without_sgr_with_mouse_reporting);
register_test!(test_alt_screen_context_menu_without_sgr_without_mouse_reporting);
register_test!(test_pane_group_state_single_pane);
register_test!(test_pane_group_state_multi_pane);
register_test!(test_pane_group_state_close_pane);
register_test!(test_pane_group_state_clear_blocks);
register_test!(test_input_syncing_is_off_by_default);
register_test!(test_can_sync_input_editor_text_in_tab);
register_test!(test_can_run_command_in_synced_panes_in_tab);
register_test!(test_synced_panes_long_running_commands);
register_test!(test_synced_inputs_terminal_mode_change_view_focus);
register_test!(test_can_bootstrap_local_bash_subshell);
register_test!(test_can_bootstrap_local_fish_subshell);
register_test!(test_can_bootstrap_local_zsh_subshell);
register_test!(test_can_bootstrap_remote_bash_subshell);
register_test!(test_can_bootstrap_remote_zsh_subshell);
register_test!(test_can_auto_bootstrap);
register_test!(test_ask_warp_ai_keybinding_for_selected_block);
register_test!(test_create_folder_from_command_palette);
register_test!(test_tab_behavior_setting);
register_test!(test_private_public_settings_routing_with_flag_enabled);
register_test!(test_private_settings_preloaded_and_not_leaked_to_toml);
register_test!(test_command_search_loads_history);
register_test!(test_histfile_left_joined_with_persisted_history);
register_test!(test_history_command_is_linked_to_local_workflow);
register_test!(test_up_arrow_history_enters_shift_tab_for_workflow);
register_test!(test_websocket_does_not_begin_on_startup);
register_test!(test_websocket_begins_on_startup);
register_test!(test_websocket_begins_after_joining_a_team);
register_test!(test_websocket_begins_after_creating_an_object);
register_test!(test_secret_is_obfuscated_on_copy);
register_test!(test_secret_tooltip_shows_on_click);
register_test!(test_secret_tooltip_respects_safe_mode_setting);
register_test!(test_copy_secret_respects_safe_mode_setting);
register_test!(test_alt_screen_secret_detection);
register_test!(test_secret_case_sensitivity);
register_test!(test_secrets_are_always_redacted_in_ai_inputs);
register_test!(test_context_chips_prompt_at_bootstrap);
register_test!(test_active_session_follows_focus);
register_test!(test_focus_panes_on_hover);
register_test!(test_close_tab_with_long_running_process);
register_test!(test_restore_single_closed_pane);
register_test!(test_restore_multiple_closed_panes);
register_test!(test_undo_close_grace_period_cleanup);
register_test!(test_closed_panes_cleared_on_rearrangement);
register_test!(test_tab_closes_when_last_visible_pane_closed);
register_test!(test_notebook_pane_tracking);
register_test!(test_close_notebook_tab);
register_test!(test_open_in_warp_banner);
register_test!(test_close_notebook_window);
register_test!(test_backspace_inside_rendered_mermaid_block_is_atomic);
// Workflow tests
register_test!(test_open_workflow_in_pane);
register_test!(test_create_personal_workflow_pane_from_command_palette);
register_test!(test_create_team_workflow_pane_from_command_palette);
register_test!(test_block_filtering_keybinding);
register_test!(test_block_filtering_keybinding_with_long_running_command);
register_test!(test_block_filtering_toolbelt_icon);
register_test!(test_block_filtering_context_menu);
register_test!(test_block_filtering_toggle_filter);
register_test!(test_block_filtering_toggle_filter_while_find_active);
register_test!(test_block_filtering_filter_then_find);
register_test!(test_block_filtering_with_secrets);
register_test!(test_block_filtering_active_block);
register_test!(test_block_filtering_clear_blocklist);
register_test!(test_autosuggestions_are_hidden_when_opening_tab_completions);
register_test!(test_latest_buffer_operations);
register_test!(test_pass_control_sequences_to_long_running_block);
register_test!(test_settings_file_migration_from_native_store);
register_test!(test_settings_file_hot_reload_applies_new_values);
register_test!(test_settings_error_banner_on_startup_with_invalid_toml);
register_test!(test_settings_error_banner_on_startup_with_invalid_value);
register_test!(test_settings_error_banner_on_reload_with_invalid_toml);
register_test!(test_settings_error_banner_on_reload_with_invalid_value);
register_test!(test_middle_click_paste);
register_test!(test_selection_first_to_last_through_ai_simple);
register_test!(test_copy_on_select_first_to_last_through_ai_simple);
register_test!(test_selection_first_to_last_through_ai_semantic);
register_test!(test_selection_first_to_last_through_ai_lines);
register_test!(test_selection_last_to_first_through_ai_simple);
register_test!(test_selection_last_to_first_through_ai_semantic);
register_test!(test_selection_last_to_first_through_ai_lines);
register_test!(test_selection_first_to_ai_simple);
register_test!(test_selection_first_to_ai_semantic);
register_test!(test_selection_first_to_ai_lines);
register_test!(test_selection_ai_to_first_simple);
register_test!(test_selection_ai_to_first_semantic);
register_test!(test_selection_ai_to_first_lines);
register_test!(test_selection_ai_to_last_simple);
register_test!(test_selection_ai_to_last_semantic);
register_test!(test_selection_ai_to_last_lines);
register_test!(test_selection_last_to_ai_simple);
register_test!(test_selection_last_to_ai_semantic);
register_test!(test_selection_last_to_ai_lines);
register_test!(test_restored_ai_block_renders_mermaid_and_local_images);
register_test!(test_agent_mode_pane_minimum_size);
register_test!(test_git_prompt_chips);
// These tests are only invoked manually, and not included in the
// automatic integration test suite.
register_test!(test_with_24_bit_color);
register_test!(test_with_long_line);
register_test!(make_1000_blocks_memory_benchmark);
register_test!(test_rule_creation);
register_test!(test_rule_update);
register_test!(test_rule_pane_opening);
register_test!(test_undo_close_stack_timeout_cleanup);
// File tree tests
register_test!(test_file_tree_opens_files_in_warp);
register_test!(test_file_tree_open_in_new_pane);
register_test!(test_file_tree_open_in_new_tab);
register_test!(test_file_tree_keyboard_navigation);
register_test!(test_file_tree_non_openable_files);
register_test!(test_file_tree_nested_file_opening);
// Go to Line tests
register_test!(test_goto_line_dialog_open_close);
register_test!(test_goto_line_jumps_to_line);
register_test!(test_goto_line_with_column);
register_test!(test_goto_line_clamps_out_of_range);
// Keyboard protocol tests
register_test!(test_keyboard_protocol_disabled_shift_enter);
register_test!(test_keyboard_protocol_enabled_shift_enter);
register_test!(test_keyboard_protocol_enabled_shifted_symbol_uses_unshifted_keycode);
register_test!(test_keyboard_protocol_query_and_apply_modes);
register_test!(test_keyboard_protocol_report_all_keys_printable_and_cursor);
register_test!(test_keyboard_protocol_event_types);
register_test!(test_keyboard_protocol_modifier_key_reporting);
register_test!(test_keyboard_protocol_modifier_self_bit);
register_test!(test_keyboard_protocol_alternate_keys_and_text);
// Video recording test (manual only)
register_test!(test_video_recording);
tests
}
+223
View File
@@ -0,0 +1,223 @@
use crate::util::{set_zsh_histfile_location, write_rc_files_for_test, ShellRcType};
use std::collections::HashMap;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::time::Duration;
use warpui::integration::{self, PersistedDataMap, TestStep};
use warpui::integration::{TestDriver, TestSetupUtils};
use warpui::{App, WindowId};
use warpui_extras::user_preferences::file_backed::FileBackedUserPreferences;
use warpui_extras::user_preferences::UserPreferences;
// We have logic in our build script to pass the path of the cargo target
// tmp directory to our app. This needs to be done as a build script because
// the relevant env var is only available at build time to ensure things like
// debuggers work correctly (https://github.com/rust-lang/cargo/pull/9375#issuecomment-824204383).
include!(concat!(env!("OUT_DIR"), "/cargo_target_tmpdir.rs"));
/// Set a test timeout of 2 minutes.
///
/// We currently configure nextest with a timeout of 60s, so 120s is a safe
/// hard timeout for the test itself. nextest should kill tests that hit the
/// timeout, but we sometimes see test processes sticking around on the test
/// runner devices, and this should help ensure those get cleaned up.
const TEST_TIMEOUT: instant::Duration = instant::Duration::from_secs(2 * 60);
/// Custom wrapper around an [`integration::Builder`] that ensures we create and setup tests in a
/// consistent way.
pub struct Builder {
inner: integration::Builder,
setup: Option<integration::SetupFn>,
user_prefs: HashMap<String, String>,
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl Builder {
pub fn new() -> Self {
let tmp_fs = PathBuf::from(cargo_target_tmpdir::get());
let mut builder = integration::Builder::new(tmp_fs).with_timeout(TEST_TIMEOUT);
if std::env::var("WARPUI_USE_REAL_DISPLAY_IN_INTEGRATION_TESTS").is_ok() {
builder = builder.with_real_display();
}
Self {
inner: builder,
setup: None,
user_prefs: Default::default(),
}
}
pub fn with_timeout(self, timeout: Duration) -> Self {
Self {
inner: self.inner.with_timeout(timeout),
..self
}
}
pub fn set_should_run_test<P>(self, predicate: P) -> Self
where
P: FnMut() -> bool + 'static,
{
Self {
inner: self.inner.set_should_run_test(predicate),
..self
}
}
pub fn with_real_display(self) -> Self {
Self {
inner: self.inner.with_real_display(),
..self
}
}
pub fn with_step(self, step: TestStep) -> Self {
Self {
inner: self.inner.with_step(step),
..self
}
}
pub fn with_steps(self, steps: Vec<TestStep>) -> Self {
Self {
inner: self.inner.with_steps(steps),
..self
}
}
/// Applies to every TestStep added after this call in the builder, unless
/// TestStep already has Some step_group_name.
pub fn with_step_group_name(self, step_group_name: &str) -> Self {
Self {
inner: self.inner.with_step_group_name(step_group_name),
..self
}
}
pub fn with_setup<C>(self, callback: C) -> Self
where
C: FnMut(&mut TestSetupUtils) + 'static,
{
assert!(
self.setup.is_none(),
"Can only register a single callback using with_setup!"
);
Self {
setup: Some(Box::new(callback)),
..self
}
}
pub fn with_cleanup<C>(self, callback: C) -> Self
where
C: FnMut(&mut TestSetupUtils) + 'static,
{
Self {
inner: self.inner.with_cleanup(callback),
..self
}
}
pub fn with_on_finish<C>(self, callback: C) -> Self
where
C: FnMut(
&mut App,
WindowId,
&mut PersistedDataMap,
) -> Pin<Box<dyn Future<Output = ()> + Send>>
+ 'static,
{
Self {
inner: self.inner.with_on_finish(callback),
..self
}
}
pub fn with_user_defaults(mut self, user_defaults: HashMap<String, String>) -> Self {
self.user_prefs.extend(user_defaults);
self
}
pub fn with_static_persisted_data(self, data: PersistedDataMap) -> Self {
Self {
inner: self.inner.with_static_persisted_data(data),
..self
}
}
/// Configures the test to run with its root directory under the /tmp
/// directory instead of under CARGO_TARGET_TMPDIR.
pub fn use_tmp_filesystem_for_test_root_directory(self) -> Self {
Self {
inner: self.inner.use_tmp_filesystem_for_test_root_directory(),
..self
}
}
pub fn build(self, test_name: &str, create_temp_dir_for_test: bool) -> TestDriver {
let Self {
inner,
mut setup,
user_prefs,
} = self;
let inner = inner.with_setup(move |utils| {
let dir = utils.test_dir();
write_rc_files_for_test(
&dir,
"",
[ShellRcType::Bash, ShellRcType::Zsh, ShellRcType::Fish],
);
set_zsh_histfile_location(&dir);
// Set the DISABLE_SAVE_ENV_VAR to make sure we don't write any keybinding changes to the
// filesystem
utils.set_env(warp::keyboard::DISABLE_SAVE_ENV_VAR, Some("true"));
// On Ubuntu (and possibly other Linux distros), a message is
// printed out during shell initialization telling the user how to
// use `sudo`. This can interfere with tests that make assertions
// about the block list, so suppress the message.
#[cfg(target_os = "linux")]
std::fs::File::create(dir.join(".sudo_as_admin_successful"))
.expect("should not fail to create file in home directory");
if let Some(ref mut callback) = setup {
callback(utils);
}
});
let driver = inner.build(test_name, create_temp_dir_for_test);
// As part of initializing the test driver, $HOME gets set to a unique
// temporary directory. We can now construct a file containing any
// initial user preferences that are needed for the test.
let file_path = warp::settings::user_preferences_file_path();
// Use println because logging may not have been initialized yet.
println!("Initializing preferences file at {file_path:?}");
let prefs = match FileBackedUserPreferences::new(file_path.clone()) {
Ok(prefs) => prefs,
Err(err) => {
eprintln!(
"Contents of existing preferences file: {:?}",
std::fs::read_to_string(file_path)
);
panic!("should not fail to initialize file-backed preferences store: {err:#}");
}
};
for (key, value) in user_prefs {
prefs
.write_value(&key, value)
.expect("should not fail to write initial user preferences");
}
driver
}
}
+10
View File
@@ -0,0 +1,10 @@
mod builder;
mod step;
pub mod test;
pub mod user_defaults;
pub mod util;
pub use builder::Builder;
pub use warp::integration_testing::view_getters;
pub use warpui::integration::TestStep;
+1
View File
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
use crate::Builder;
use warp::integration_testing::{
step::new_step_with_default_assertions,
terminal::{
assert_selected_block_index_is_last_renderable, execute_command_for_single_terminal_in_tab,
util::ExpectedExitStatus, wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::ai_assistant_panel_view,
};
use warpui::async_assert;
use super::new_builder;
/// Checks if the Ask Warp AI keybinding works correctly when a block is selected.
/// This is a regression test: https://linear.app/warpdotdev/issue/WAR-6758/warp-ai-ask-from-block-keybinding-doesnt-work-as-expected.
pub fn test_ask_warp_ai_keybinding_for_selected_block() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command_for_single_terminal_in_tab(
0,
String::from("echo foo"),
ExpectedExitStatus::Success,
"foo",
))
.with_step(
new_step_with_default_assertions("select block")
.with_keystrokes(&["cmdorctrl-up"])
.add_named_assertion(
"ensure block is selected",
assert_selected_block_index_is_last_renderable(),
),
)
.with_step(
new_step_with_default_assertions("select block")
.with_keystrokes(&["ctrl-shift-space"])
.add_named_assertion("ask warp ai from selected block", |app, window_id| {
let ai_assistant_panel = ai_assistant_panel_view(app, window_id);
ai_assistant_panel.read(app, |view, ctx| {
let expected_code_block = "```warp\nfoo\n```";
let editor_content = view.editor().as_ref(ctx).buffer_text(ctx);
async_assert!(editor_content.contains(expected_code_block))
})
}),
)
}
@@ -0,0 +1,352 @@
use crate::test::integration_testing::block_filtering::{
open_block_filter_editor, open_block_filter_editor_for_long_running_command,
open_block_filter_editor_via_keybinding,
open_block_filter_editor_via_keybinding_long_running_command,
};
use crate::test::integration_testing::block_filtering::{
LongRunningCommandTestCase, SecretTestCase, SimpleTestCase,
};
use crate::test::integration_testing::secret_redaction::assert_secret_tooltip_open;
use crate::test::integration_testing::terminal::{
clear_blocklist_to_remove_bootstrapped_blocks, hover_over_block_zero,
};
use crate::test::new_step_with_default_assertions;
use crate::test::toggle_setting;
use crate::test::TestStep;
use warp::cmd_or_ctrl_shift;
use warp::integration_testing::terminal::util::current_shell_starter_and_version;
use warp::integration_testing::terminal::{
assert_context_menu_is_open, initialize_secret_regexes,
wait_until_bootstrapped_single_pane_for_tab,
};
use warp::integration_testing::view_getters::single_terminal_view_for_tab;
use warp::settings_view::PrivacyPageAction;
use warp::settings_view::SettingsAction;
use warp::terminal::model::index::Point;
use warp::terminal::model::terminal_model::{BlockIndex, WithinBlock, WithinModel};
use warp::terminal::shell::ShellType;
use warp::terminal::GridType;
use warpui::{async_assert, async_assert_eq};
use crate::Builder;
use super::new_builder;
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_keybinding() -> Builder {
new_builder()
.set_should_run_test(|| cfg!(target_os = "macos"))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(open_block_filter_editor_via_keybinding())
.with_step(SimpleTestCase::perform_filter_query())
}
pub fn test_block_filtering_keybinding_with_long_running_command() -> Builder {
new_builder()
.set_should_run_test(|| cfg!(target_os = "macos"))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_steps(LongRunningCommandTestCase::enter_input_into_cat())
.with_step(open_block_filter_editor_via_keybinding_long_running_command())
.with_step(LongRunningCommandTestCase::perform_filter_query())
.with_step(LongRunningCommandTestCase::exit_long_running_command())
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_toolbelt_icon() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(hover_over_block_zero())
.with_step(
new_step_with_default_assertions("Open block filter editor via toolbelt icon")
.with_click_on_saved_position("filter_button_for_block_0")
.add_named_assertion("Assert that block filter is open", |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.active_filter_editor_block_index(),
Some(BlockIndex::zero())
)
})
}),
)
.with_step(SimpleTestCase::perform_filter_query())
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_context_menu() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(hover_over_block_zero())
.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("Toggle block filter")
.add_named_assertion("Assert that block filter is open", |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.active_filter_editor_block_index(),
Some(BlockIndex::zero())
)
})
}),
)
.with_step(SimpleTestCase::perform_filter_query())
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_toggle_filter() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(open_block_filter_editor())
.with_step(SimpleTestCase::perform_filter_query())
.with_step(hover_over_block_zero())
.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("Toggle block filter off")
.with_click_on_saved_position("Toggle block filter")
.add_named_assertion(
"Assert that the block filter editor is not open",
|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _ctx| {
async_assert!(view.active_filter_editor_block_index().is_none())
})
},
)
.add_named_assertion(
"Assert that all lines are present after toggling off filter",
|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 displayed_output_rows = model
.block_list()
.last_non_hidden_block()
.expect("No last non-hidden block found.")
.displayed_output_rows();
async_assert!(displayed_output_rows.is_none())
})
},
),
)
.with_step(hover_over_block_zero())
.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("Toggle block filter on")
.with_click_on_saved_position("Toggle block filter")
.add_named_assertion(
"Assert that block filter is applied",
SimpleTestCase::assert_filter_is_applied(),
)
.add_named_assertion("Assert that block filter is open", |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.active_filter_editor_block_index(),
Some(BlockIndex::zero())
)
})
}),
)
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_toggle_filter_while_find_active() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(
new_step_with_default_assertions("Open find bar")
.with_keystrokes(&[cmd_or_ctrl_shift("f")])
.with_typed_characters(&["line"])
.add_named_assertion("Assert that there 6 find matches", |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,
6,
"Expected six matches but got {:?}",
num_matches
)
}),
)
.with_step(open_block_filter_editor())
.with_step(SimpleTestCase::perform_filter_query().add_named_assertion(
"Assert that we have 4 find matches after filtering",
|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("Open find bar and clear find query")
.with_keystrokes(&[cmd_or_ctrl_shift("a"), "backspace".to_string()])
.add_named_assertion(
"Assert that there 6 find matches again",
|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,
6,
"Expected six matches but got {:?}",
num_matches
)
},
),
)
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_filter_then_find() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(SimpleTestCase::execute_command())
.with_step(open_block_filter_editor())
.with_step(SimpleTestCase::perform_filter_query())
.with_step(
new_step_with_default_assertions("Open find bar")
.with_keystrokes(&[cmd_or_ctrl_shift("f")])
.with_typed_characters(&["line"])
.add_named_assertion("Assert that there 4 find matches", |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
)
}),
)
}
pub fn test_block_filtering_with_secrets() -> Builder {
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 | ShellType::PowerShell
)
})
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_step(SecretTestCase::execute_command())
.with_step(open_block_filter_editor())
.with_step(SecretTestCase::perform_filter_query())
.with_step(
// Note: ideally, we shouldn't hardcode a secret handle ID here but we're doing this
// for now. This is affected by the addition/removal of new `GridType`s!
new_step_with_default_assertions("Click on secret to show tooltip")
.with_click_on_saved_position("terminal_view:first_cell_in_secret_1")
.add_assertion(assert_secret_tooltip_open(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 model = view.model.lock();
let secret =
model.secret_at_point(&WithinModel::BlockList(WithinBlock::new(
Point::new(0, 24),
BlockIndex::zero(),
GridType::Output,
)));
async_assert!(secret.is_some(), "Secret exists")
})
}),
)
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_active_block() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_steps(LongRunningCommandTestCase::enter_input_into_cat())
.with_step(open_block_filter_editor_for_long_running_command())
.with_step(LongRunningCommandTestCase::perform_filter_query())
.with_step(LongRunningCommandTestCase::exit_long_running_command())
}
// TODO(CORE-2721): Block count / index Failed b/c of in-band generators
pub fn test_block_filtering_clear_blocklist() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_steps(LongRunningCommandTestCase::enter_input_into_cat())
.with_step(open_block_filter_editor_for_long_running_command())
.with_step(LongRunningCommandTestCase::perform_filter_query())
.with_step(
TestStep::new("Escape block filter editor and clear blocklist")
.with_keystrokes(&["escape", cmd_or_ctrl_shift("k").as_str()])
.add_named_assertion(
"Assert that only the cursor line is included in the displayed rows",
|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 displayed_output_rows = model
.block_list()
.active_block()
.displayed_output_rows()
.expect("No displayed output rows found.")
.collect::<Vec<_>>();
async_assert_eq!(displayed_output_rows, vec![0])
})
},
),
)
.with_step(LongRunningCommandTestCase::exit_long_running_command())
}
@@ -0,0 +1,378 @@
//! Integration tests for bootstrapping logic.
use settings::Setting as _;
use version_compare::Cmp;
use warp::{
cmd_or_ctrl_shift,
integration_testing::{
input::{
input_contains_string, input_editor_is_focused, input_editor_is_not_focused,
input_is_empty,
},
step::new_step_with_default_assertions,
tab::tab_title_step,
terminal::{
assert_active_block_command_for_single_terminal_in_tab,
assert_long_running_block_executing_for_single_terminal_in_tab,
execute_command_for_single_terminal_in_tab,
util::{current_shell_starter_and_version, ExpectedExitStatus},
wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::{single_input_view_for_tab, single_terminal_view_for_tab},
},
terminal::session_settings::HonorPS1,
terminal::shell::{self, ShellType},
workspace::Workspace,
};
use warpui::{
async_assert, async_assert_eq, clipboard::ClipboardContent, integration::TestStep, ViewHandle,
};
use crate::util::{write_all_rc_files_for_test, write_rc_files_for_test, ShellRcType};
use super::{new_builder, Builder};
/// Ensures that config files are only sourced once when bootstrapping a new session.
pub fn test_rc_files_only_sourced_once_during_bootstrapping() -> Builder {
new_builder()
.with_setup(|utils| {
let dir = utils.test_dir();
write_all_rc_files_for_test(dir, r"echo 'foo' >> ~/rc_output");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Currently, when starting the application, we invoke a one-off non-interactive, login
// shell that is not _associated_ to any particular session (see `LocalShell::new`).
// Since this test only cares about the fact that the config files are sourced
// once _per session_, we clear the effects of this one-off initialization
// and then ensure that starting a session only sources config files once.
.with_step(execute_command_for_single_terminal_in_tab(
0,
"rm ~/rc_output".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Add a new session")
.with_keystrokes(&[cmd_or_ctrl_shift("t")]),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(execute_command_for_single_terminal_in_tab(
1,
"cat ~/rc_output".to_string(),
ExpectedExitStatus::Success,
"foo",
))
}
pub fn test_unescaped_prompt_bootstraps() -> Builder {
new_builder()
.with_setup(|utils| {
let dir = utils.test_dir();
write_rc_files_for_test(
dir,
r"export PATH=/Applications/VMware\\\ Fusion.app/Contents/Public:$PATH",
[ShellRcType::Bash, ShellRcType::Zsh],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
}
// TODO: test doesn't work on fish because no block is created from output of config.fish
pub fn test_paste_and_type_characters_before_bootstrap() -> Builder {
new_builder()
.set_should_run_test(|| {
let (starter, _version) = current_shell_starter_and_version();
!matches!(starter.shell_type(), ShellType::Fish)
})
.with_setup(|utils| {
let dir = utils.test_dir();
write_rc_files_for_test(&dir, "echo -n 'Enter some user input: ' && read", [ShellRcType::Zsh, ShellRcType::Bash, ShellRcType::Fish]);
write_rc_files_for_test(&dir, "Read-Host 'Enter some user input'", [ShellRcType::PowerShell]);
// On Ubuntu (and possibly other Linux distros), a message is
// printed out during shell initialization telling the user how to
// use `sudo`. This interferes with our expected pty contents, so suppress the message.
if cfg!(target_os = "linux") {
std::fs::File::create(dir.join(".sudo_as_admin_successful"))
.expect("should not fail to create file in home directory");
}
})
.with_step(
TestStep::new("Wait for rc file to run")
.add_named_assertion("Long running block executing", assert_long_running_block_executing_for_single_terminal_in_tab(false, 0))
// Output pre-bootstrap writes to the block's command, not the output, as nothing causes
// us to finish the command grid.
.add_named_assertion("Validate block contents", assert_active_block_command_for_single_terminal_in_tab("Enter some user input: ", 0))
)
.with_step(
TestStep::new("Warp input should not start focused, since .rc file is reading user input")
.add_assertion(input_editor_is_not_focused(0))
)
.with_step(
TestStep::new("Populate the clipboard")
.with_action(|app, _, _| {
app.update(|app| {
app.clipboard().write(ClipboardContent::plain_text("this is the pasted text".to_string()))
})
})
.add_assertion(|app, _| {
app.update(|app| {
async_assert_eq!(
app.clipboard().read().plain_text,
String::from("this is the pasted text"),
"Clipboard should be populated",
)
})
})
// Make sure the input is not focused before we paste, even though we checked this in an earlier step.
// The input is sometimes focused for a brief moment causing the paste to go to the wrong place without this check for some reason.
.add_assertion(input_editor_is_not_focused(0)),
)
.with_step(
TestStep::new("Pasted text go into the pty and not warp input")
.with_keystrokes(&[cmd_or_ctrl_shift("v")])
.add_named_assertion("Input should be empty", input_is_empty(0))
.add_named_assertion("Pasted text should go to pty", |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _| {
async_assert_eq!(
view.model.lock().block_list().active_block().command_to_string(),
String::from("Enter some user input: this is the pasted text"),
"Paste should go to pty"
)
})
})
// Make sure the input is not focused before we type, even though we checked this in an earlier step.
// The input is sometimes focused for a brief moment causing typed characters to go to the wrong place without this check for some reason.
.add_named_assertion("Input should not be focused", input_editor_is_not_focused(0)),
)
.with_step(
TestStep::new("Typed characters should go to the pty and not warp input")
.with_typed_characters(&["these are some typed characters"])
.add_named_assertion("Input should be empty", input_is_empty(0))
.add_named_assertion("Typed characters should go to pty", |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _| {
async_assert_eq!(
view.model.lock().block_list().active_block().command_to_string(),
String::from("Enter some user input: this is the pasted textthese are some typed characters"),
"Typed characters should go to pty"
)
})
})
)
.with_step(
TestStep::new("Click on input to focus the input box")
.with_click_on_saved_position_fn(|app, window_id| {
let input = single_input_view_for_tab(app, window_id, 0);
input.read(app, |input, _| {
input.save_position_id()
})
})
.add_assertion(input_editor_is_focused(0)),
)
.with_step(
TestStep::new("Pasted text should go in input since input is focused")
.with_keystrokes(&[cmd_or_ctrl_shift("v")])
.add_assertion(input_contains_string(0, "this is the pasted text".to_owned()))
)
.with_step(
TestStep::new("Typed characters should go in input since input is focused")
.with_typed_characters(&["these are some typed characters"])
.add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned())),
)
.with_step(
TestStep::new("Focus the terminal view")
.with_click_on_saved_position_fn(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _| {
view.content_element_position_id().to_owned()
})
})
.add_assertion(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let focused_view_id = app.focused_view_id(window_id).expect("Focused view should exist");
async_assert!(focused_view_id == terminal_view.id(), "Terminal should be focused")
}),
)
.with_step(
TestStep::new("Press enter to finish user input and allow bootstrapping to finish")
.with_keystrokes(&["enter"]),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Warp input should be focused and keep buffered text")
.add_assertion(input_editor_is_focused(0))
.add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned()))
)
}
pub fn test_bootstrap_with_no_script_execution_block() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Ensure there are no visible blocks")
.with_keystrokes(&["cmdorctrl-up"])
.add_assertion(move |app, window_id| {
let views: Vec<ViewHandle<Workspace>> = app.views_of_type(window_id).unwrap();
let workspace = views.first().unwrap();
let terminal_view = workspace.read(app, |workspace, _| {
workspace
.get_pane_group_view_unchecked(0)
.read(app, |pane_group, ctx| {
pane_group
.terminal_view_at_pane_index(0, ctx)
.expect("View should be defined at pane 0")
.clone()
})
});
terminal_view.read(app, |view, _| {
async_assert!(
view.selected_blocks_tail_index().is_none(),
"There should not be any blocks the user can select"
)
})
}),
)
}
pub fn test_instant_prompt_bootstrap() -> Builder {
new_builder()
.set_should_run_test(|| {
// Only run this one on zsh
let (starter, _) = current_shell_starter_and_version();
matches!(starter.shell_type(), shell::ShellType::Zsh)
})
.with_setup(|utils| {
// If the instant prompt var is not set to off, hang forever.
let dir = utils.test_dir();
write_rc_files_for_test(
dir,
r#"if [[ "$POWERLEVEL9K_INSTANT_PROMPT" != "off" ]]; then read; fi"#,
[ShellRcType::Zsh],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
}
/// Ensure this issue doesn't happen again.
/// https://github.com/warpdotdev/Warp/issues/2636
/// Bootstrapping was failing when PROMPT_COMMAND was an array
pub fn test_bash_bootstraps_with_prompt_command_array() -> Builder {
new_builder()
.set_should_run_test(|| {
// Only run this one on bash
let (starter, version) = current_shell_starter_and_version();
matches!(starter.shell_type(), shell::ShellType::Bash)
&& version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false)
})
.with_setup(|utils| {
let dir = utils.test_dir();
write_rc_files_for_test(
dir,
r#"
PROMPT_COMMAND=('printf "\033]0;TEST_TAB_TITLE\a"' 'echo hello')
"#,
[ShellRcType::Bash],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command_for_single_terminal_in_tab(
0, /*tab_idx*/
"ls".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(tab_title_step(
"Assert the user's tab title used",
"TEST_TAB_TITLE".to_string(),
))
}
/// Ensures that the zsh bootstrap script's precmd/preexec hooks work correctly when the user
/// has `setopt nounset` (set -u) enabled. This option causes the shell to error when referencing
/// unset variables, so we need to use `${VAR:-}` syntax for external environment variables.
///
/// The test enables nounset after bootstrap completes, then runs commands to trigger the
/// precmd and preexec hooks which reference various environment variables.
pub fn test_zsh_bootstraps_with_nounset_option() -> Builder {
new_builder()
.set_should_run_test(|| {
// Only run this one on zsh
let (starter, _) = current_shell_starter_and_version();
matches!(starter.shell_type(), shell::ShellType::Zsh)
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Enable nounset after bootstrap, then run a command to trigger precmd/preexec hooks
.with_step(execute_command_for_single_terminal_in_tab(
0,
"setopt nounset".to_string(),
ExpectedExitStatus::Success,
(),
))
// Run another command to verify precmd/preexec work with nounset enabled
.with_step(execute_command_for_single_terminal_in_tab(
0,
"echo 'nounset test passed'".to_string(),
ExpectedExitStatus::Success,
"nounset test passed",
))
}
pub fn test_bash_bootstraps_with_prompt_command_array_that_sets_ps1() -> Builder {
new_builder()
.set_should_run_test(|| {
// Only run this one on bash
let (starter, version) = current_shell_starter_and_version();
matches!(starter.shell_type(), shell::ShellType::Bash)
&& version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false)
})
.with_user_defaults(std::collections::HashMap::from([(
HonorPS1::storage_key().to_owned(),
true.to_string(),
)]))
.with_setup(|utils| {
let dir = utils.test_dir();
write_rc_files_for_test(
dir,
r#"
function custom_prompt() {
PS1="darmok@tanagra$ "
}
PROMPT_COMMAND=('printf "\033]0;SHAKA WHEN THE WALLS FELL\a"' 'custom_prompt')
"#,
[ShellRcType::Bash],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command_for_single_terminal_in_tab(
0, /*tab_idx*/
"ls".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(tab_title_step(
"Assert the user's tab title used",
"SHAKA WHEN THE WALLS FELL".to_string(),
))
.with_step(
new_step_with_default_assertions("Check PS1 value").add_assertion(
move |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let prompt = terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let block = model
.block_list()
.blocks()
.last()
.expect("After bootstrapping, we should have a block");
block.prompt_to_string()
});
async_assert_eq!(
prompt,
"darmok@tanagra$ ",
"prompt should be 'darmok@tanagra$ ' but got '{prompt}' instead"
)
},
),
)
}
+665
View File
@@ -0,0 +1,665 @@
use std::{
fs,
path::{Path, PathBuf},
time::Duration,
};
use command::blocking::Command;
use warp::features::FeatureFlag;
use warp::{
integration_testing::{
code_review::{
assert_code_review_anchor, assert_code_review_line_text, assert_code_review_loaded,
assert_code_review_scroll_region, scroll_code_review_to_deleted_range,
scroll_code_review_to_footer, scroll_code_review_to_header, scroll_code_review_to_line,
ScrollRegion,
},
terminal::wait_until_bootstrapped_single_pane_for_tab,
view_getters::{single_terminal_view_for_tab, workspace_view},
},
workspace::WorkspaceAction,
};
use warpui::{
async_assert,
integration::{AssertionCallback, TestStep},
App, WindowId,
};
use crate::{util::write_all_rc_files_for_test, Builder};
use super::new_builder;
const TEST_FILE_NAME: &str = "scroll_target.txt";
const TARGET_LINE_NUMBER: usize = 70;
const INSERT_ABOVE_LINE_NUMBER: usize = 15;
const INSERT_BELOW_LINE_NUMBER: usize = 250;
const INSERTED_LINE_COUNT: usize = 10;
const TOTAL_LINE_COUNT: usize = 400;
fn base_line_text(line_number: usize) -> String {
format!("line {line_number:03}")
}
fn modified_line_text(line_number: usize) -> String {
format!("line {line_number:03} modified")
}
fn initial_committed_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.map(|line_number| format!("{}\n", base_line_text(line_number)))
.collect()
}
fn initial_diff_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.map(|line_number| {
let line_text =
if (10..=80).contains(&line_number) || (200..=300).contains(&line_number) {
modified_line_text(line_number)
} else {
base_line_text(line_number)
};
format!("{line_text}\n")
})
.collect()
}
fn inserted_lines(prefix: &str) -> Vec<String> {
(1..=INSERTED_LINE_COUNT)
.map(|index| format!("{prefix} inserted {index:02}"))
.collect()
}
fn insert_lines(path: &Path, before_line_number: usize, new_lines: &[String]) {
let contents = fs::read_to_string(path).expect("should read test file");
let mut lines: Vec<String> = contents.lines().map(ToOwned::to_owned).collect();
let insert_index = before_line_number.saturating_sub(1);
lines.splice(insert_index..insert_index, new_lines.iter().cloned());
fs::write(path, format!("{}\n", lines.join("\n"))).expect("should rewrite test file");
}
fn run_git(test_dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(test_dir)
.status()
.expect("git command should run");
assert!(status.success(), "git {:?} should succeed", args);
}
fn open_code_review_panel(app: &mut App, window_id: WindowId) {
let workspace = workspace_view(app, window_id);
app.update(|ctx| {
ctx.dispatch_typed_action_for_view(
window_id,
workspace.id(),
&WorkspaceAction::ToggleRightPanel,
);
});
}
fn assert_repo_detected() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |terminal_view, _ctx| {
async_assert!(
terminal_view.current_repo_path().is_some(),
"expected the active terminal to detect a git repository"
)
})
})
}
fn scroll_code_review_to_target_line() -> TestStep {
scroll_code_review_to_line(PathBuf::from(TEST_FILE_NAME), TARGET_LINE_NUMBER)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_anchor(
PathBuf::from(TEST_FILE_NAME),
modified_line_text(TARGET_LINE_NUMBER),
Some(TARGET_LINE_NUMBER),
))
// Allow the scroll debounce (150ms) to fire so that the stored
// scroll context is captured before the next step mutates the file.
.set_post_step_pause(Duration::from_millis(250))
}
fn mutate_test_file(before_line_number: usize, prefix: &'static str) -> TestStep {
TestStep::new(&format!(
"Insert {INSERTED_LINE_COUNT} lines at {before_line_number}"
))
.with_action(move |app, window_id, _| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let cwd = terminal_view
.read(app, |terminal_view, _ctx| terminal_view.pwd())
.expect("terminal should expose current working directory");
let file_path = PathBuf::from(cwd).join(TEST_FILE_NAME);
let new_lines = inserted_lines(prefix);
insert_lines(&file_path, before_line_number, &new_lines);
})
.set_post_step_pause(Duration::from_millis(250))
}
fn code_review_scroll_anchor_builder(
insertion_line_number: usize,
insertion_prefix: &'static str,
) -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines(insertion_prefix)
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(repo_dir.join(TEST_FILE_NAME), initial_diff_contents())
.expect("should write initial diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(scroll_code_review_to_target_line())
.with_step(mutate_test_file(insertion_line_number, insertion_prefix))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
PathBuf::from(TEST_FILE_NAME),
insertion_line_number,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Wait for code review to preserve the visible anchor text")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_anchor(
PathBuf::from(TEST_FILE_NAME),
modified_line_text(TARGET_LINE_NUMBER),
None,
)),
)
}
pub fn test_code_review_scroll_anchor_preserved_when_inserting_above() -> Builder {
code_review_scroll_anchor_builder(INSERT_ABOVE_LINE_NUMBER, "above")
}
pub fn test_code_review_scroll_anchor_unchanged_when_inserting_below() -> Builder {
code_review_scroll_anchor_builder(INSERT_BELOW_LINE_NUMBER, "below")
}
// --- Multi-file test ---
// Tests that scroll preservation works when scrolled to the second file in the
// code review list. This exercises the adjustment callback returning an
// item-relative offset (not absolute), which only matters for index > 0.
const SECOND_FILE_NAME: &str = "second_file.txt";
const FIRST_FILE_NAME: &str = "first_file.txt";
const MULTI_FILE_TARGET_LINE: usize = 70;
const MULTI_FILE_INSERT_LINE: usize = 15;
fn multi_file_base_line(file_prefix: &str, line_number: usize) -> String {
format!("{file_prefix} line {line_number:03}")
}
fn multi_file_modified_line(file_prefix: &str, line_number: usize) -> String {
format!("{file_prefix} line {line_number:03} modified")
}
fn multi_file_committed_contents(file_prefix: &str) -> String {
(1..=TOTAL_LINE_COUNT)
.map(|n| format!("{}\n", multi_file_base_line(file_prefix, n)))
.collect()
}
fn multi_file_diff_contents(file_prefix: &str) -> String {
(1..=TOTAL_LINE_COUNT)
.map(|n| {
let text = if (10..=80).contains(&n) || (200..=300).contains(&n) {
multi_file_modified_line(file_prefix, n)
} else {
multi_file_base_line(file_prefix, n)
};
format!("{text}\n")
})
.collect()
}
fn mutate_named_file(
file_name: &'static str,
before_line_number: usize,
prefix: &'static str,
) -> TestStep {
TestStep::new(&format!(
"Insert {INSERTED_LINE_COUNT} lines at {before_line_number} in {file_name}"
))
.with_action(move |app, window_id, _| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let cwd = terminal_view
.read(app, |terminal_view, _ctx| terminal_view.pwd())
.expect("terminal should expose current working directory");
let file_path = PathBuf::from(cwd).join(file_name);
let new_lines = inserted_lines(prefix);
insert_lines(&file_path, before_line_number, &new_lines);
})
.set_post_step_pause(Duration::from_millis(250))
}
// --- Deleted range test ---
// Tests that scroll preservation works when scrolled to a deleted (removed) line
// region. This exercises the RemovedLine variant of RelocatableScrollContext.
const DELETED_RANGE_START: usize = 61;
const DELETED_RANGE_END: usize = 80;
/// Current buffer line just before the deleted range. The temporary blocks
/// for deleted lines 61-80 appear immediately after this line in the diff.
const DELETED_RANGE_NEAR_LINE: usize = 60;
fn deleted_range_diff_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.filter(|&n| !(DELETED_RANGE_START..=DELETED_RANGE_END).contains(&n))
.map(|n| {
let text = if (200..=300).contains(&n) {
modified_line_text(n)
} else {
base_line_text(n)
};
format!("{text}\n")
})
.collect()
}
pub fn test_code_review_scroll_preserved_deleted_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("above")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
// Write diff contents that DELETE lines 61-80
fs::write(repo_dir.join(TEST_FILE_NAME), deleted_range_diff_contents())
.expect("should write deleted range diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_deleted_range(
PathBuf::from(TEST_FILE_NAME),
DELETED_RANGE_NEAR_LINE,
)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::RemovedLine))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_test_file(INSERT_ABOVE_LINE_NUMBER, "above"))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
PathBuf::from(TEST_FILE_NAME),
INSERT_ABOVE_LINE_NUMBER,
inserted_line_text,
))
// Allow time for the asynchronous diff recomputation to complete.
// Without this, the assertion below may pass against the stale
// (pre-recompute) layout where temporary blocks haven't moved.
.set_post_step_pause(Duration::from_millis(1000)),
)
.with_step(
TestStep::new("Assert scroll is still in the deleted range after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::RemovedLine)),
)
}
// --- Header range test ---
// Tests that scroll preservation works when scrolled to the file header region.
// This exercises the Header variant of RelocatableScrollContext.
pub fn test_code_review_scroll_preserved_header_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("above")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(repo_dir.join(TEST_FILE_NAME), initial_diff_contents())
.expect("should write initial diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_header(PathBuf::from(TEST_FILE_NAME))
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Header))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_test_file(INSERT_ABOVE_LINE_NUMBER, "above"))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
PathBuf::from(TEST_FILE_NAME),
INSERT_ABOVE_LINE_NUMBER,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Assert scroll is still in the header region after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Header)),
)
}
// --- Footer range test ---
// Tests that scroll preservation works when scrolled past the editor content
// into the footer region. This exercises the Footer variant of RelocatableScrollContext.
//
// The footer region of a file is only reachable when there is a sufficiently
// tall file below it in the list; otherwise the list's max-scroll clamping
// prevents scrolling past the editor content. This test reuses the multi-file
// helpers (FIRST_FILE_NAME / SECOND_FILE_NAME) so that first_file.txt (index 0)
// has second_file.txt below it, making the footer reachable.
pub fn test_code_review_scroll_preserved_footer_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("first")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
// Two files: first_file.txt at index 0, second_file.txt at index 1.
// We scroll to the footer of the first file; the second file
// provides enough total list height so the footer is reachable.
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_committed_contents("first"),
)
.expect("should write first file committed contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_committed_contents("second"),
)
.expect("should write second file committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", FIRST_FILE_NAME, SECOND_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_diff_contents("first"),
)
.expect("should write first file diff contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_diff_contents("second"),
)
.expect("should write second file diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_footer(PathBuf::from(FIRST_FILE_NAME))
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Footer))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_named_file(
FIRST_FILE_NAME,
MULTI_FILE_INSERT_LINE,
"first",
))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
PathBuf::from(FIRST_FILE_NAME),
MULTI_FILE_INSERT_LINE,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Assert scroll is still in the footer region after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Footer)),
)
}
pub fn test_code_review_scroll_preserved_second_file() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("second")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
// Create and commit two files. File names sort alphabetically so
// first_file.txt appears at index 0 and second_file.txt at index 1.
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_committed_contents("first"),
)
.expect("should write first file committed contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_committed_contents("second"),
)
.expect("should write second file committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", FIRST_FILE_NAME, SECOND_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
// Write modified versions to create diffs in both files
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_diff_contents("first"),
)
.expect("should write first file diff contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_diff_contents("second"),
)
.expect("should write second file diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
// Scroll to a target line in the SECOND file (index 1)
.with_step(
scroll_code_review_to_line(PathBuf::from(SECOND_FILE_NAME), MULTI_FILE_TARGET_LINE)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_anchor(
PathBuf::from(SECOND_FILE_NAME),
multi_file_modified_line("second", MULTI_FILE_TARGET_LINE),
Some(MULTI_FILE_TARGET_LINE),
))
.set_post_step_pause(Duration::from_millis(250)),
)
// Insert lines above the target in the second file
.with_step(mutate_named_file(
SECOND_FILE_NAME,
MULTI_FILE_INSERT_LINE,
"second",
))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines in second file")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
PathBuf::from(SECOND_FILE_NAME),
MULTI_FILE_INSERT_LINE,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Wait for code review to preserve the visible anchor in second file")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_anchor(
PathBuf::from(SECOND_FILE_NAME),
multi_file_modified_line("second", MULTI_FILE_TARGET_LINE),
None,
)),
)
}
+112
View File
@@ -0,0 +1,112 @@
//! Integration tests for CTRL-D / EOT behaviour.
use warp::{
integration_testing::{
step::new_step_with_default_assertions,
terminal::{
assert_active_block_command_for_single_terminal_in_tab, assert_bootstrapping_stage,
assert_no_block_executing, assert_terminal_bootstrapped,
execute_python_interpreter_in_tab, util::current_shell_starter_and_version,
wait_until_bootstrapped_single_pane_for_tab, PYTHON_PROMPT_READY,
},
view_getters::assert_no_views_of_type,
},
pane_group::PaneGroup,
terminal::{model::bootstrap::BootstrapStage, shell::ShellType, TerminalView},
workspace::Workspace,
};
use warpui::integration::TestStep;
use super::{new_builder, Builder};
use crate::util::write_all_rc_files_for_test;
/// Verifies that ctrl-d correctly sends EOT to long-running commands.
pub fn test_ctrl_d_eot() -> 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("Check ctrl-d terminates the command")
.with_keystrokes(&["ctrl-d"])
.add_assertion(assert_no_block_executing(0, 0)),
)
}
// TODO(zheng) Add ctrl-d to exit the window too, after stopping this from beachballing.
pub fn test_ctrl_d_exit() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new(
"ctrl-d should exit the shell and the corresponding pane / tab should be closed",
)
.with_keystrokes(&["ctrl-d"])
.add_assertion(assert_no_views_of_type::<TerminalView>())
.add_assertion(assert_no_views_of_type::<PaneGroup>())
.add_assertion(assert_no_views_of_type::<Workspace>()),
)
}
/// Tests that CTRL-D will complete a blocking read during bootstrapping
/// (e.g. omz update) by writing EOT to the PTY (which will result in an EOF condition).
pub fn test_ctrl_d_handled_by_read_during_bootstrapping() -> Builder {
new_builder()
.set_should_run_test(|| {
let (starter, _) = current_shell_starter_and_version();
// We need to fix https://github.com/warpdotdev/Warp/issues/1869 before
// this test works on fish.
!matches!(starter.shell_type(), ShellType::Fish)
})
.with_setup(|utils| {
let dir = utils.test_dir();
write_all_rc_files_for_test(dir, "python3");
})
.with_step(
TestStep::new("Make sure shell is still bootstrapping")
.add_assertion(assert_bootstrapping_stage(0, 0, BootstrapStage::ScriptExecution))
// When the client's RC files produce output, they appear in the command grid of the
// active block.
.add_assertion(assert_active_block_command_for_single_terminal_in_tab(
&*PYTHON_PROMPT_READY,
0,
))
)
.with_step(
TestStep::new("ctrl-d should write EOT to PTY which should signal EOF to python and bootstrapping should finish")
.with_keystrokes(&["ctrl-d"])
.add_assertion(assert_terminal_bootstrapped(0, 0)),
)
}
/// Tests that entering CTRL-D while the PTY is bootstrapping
/// will exit the shell once bootstrapping is done
/// (assuming nothing else consumed EOT during bootstrapping).
pub fn test_ctrl_d_during_bootstrapping_exits_shell_upon_completion() -> Builder {
let test = new_builder()
.with_setup(|utils| {
let dir = utils.test_dir();
write_all_rc_files_for_test(dir, "sleep 2");
})
.with_step(
TestStep::new("Make sure shell is still bootstrapping").add_assertion(
assert_bootstrapping_stage(0, 0, BootstrapStage::ScriptExecution),
),
);
let final_test_step =
TestStep::new("ctrl-d should write EOT to PTY which should exit the shell process")
.with_keystrokes(&["ctrl-d"]);
// On MacOS, the common behaviour for writing ctrl-d while bootstrapping
// is to exit upon completion.
// However, this differs on Linux where the common behaviour is to
// ignore ctrl-d (and the corresponding EOF) while bootstrapping.
let final_assertion = if cfg!(target_os = "linux") {
assert_terminal_bootstrapped(0, 0)
} else {
// TODO: figure out what the right behaviour is on windows.
assert_no_views_of_type::<TerminalView>()
};
test.with_step(final_test_step.add_assertion(final_assertion))
}
+305
View File
@@ -0,0 +1,305 @@
use super::{new_builder, Builder};
use regex::Regex;
use warp::{
integration_testing::{
step::new_step_with_default_assertions,
tab::assert_pane_title,
terminal::wait_until_bootstrapped_single_pane_for_tab,
view_getters::{pane_group_view, workspace_view},
},
workspace::WorkspaceAction,
};
use warpui::{async_assert, async_assert_eq, integration::TestStep, App};
use crate::util::write_all_rc_files_for_test;
fn open_file_tree_panel(app: &mut App) {
let window_id = app.read(|ctx| {
ctx.windows()
.active_window()
.expect("should have active window")
});
let workspace = workspace_view(app, window_id);
app.update(|ctx| {
ctx.dispatch_typed_action_for_view(
window_id,
workspace.id(),
&WorkspaceAction::ToggleProjectExplorer,
);
});
}
/// Test that clicking a file in the file tree opens it in Warp's editor.
/// This is a regression test for the bug where files were being opened in
/// external editors instead of Warp's built-in editor.
pub fn test_file_tree_opens_files_in_warp() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
// Change to the test directory
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
// Create a test file
std::fs::write(test_dir.join("test_file.txt"), "Hello from test file!")
.expect("Failed to create test file");
// Create a test directory with a file inside
std::fs::create_dir_all(test_dir.join("test_dir"))
.expect("Failed to create test directory");
std::fs::write(
test_dir.join("test_dir/nested_file.rs"),
"fn main() {\n println!(\"Hello, world!\");\n}",
)
.expect("Failed to create nested file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
// Click on test_file.txt in the file tree
.with_step(
new_step_with_default_assertions("Click on test_file.txt in file tree")
.with_click_on_saved_position("file_tree_item:test_file.txt")
.add_assertion(|app, window_id| {
// Verify that a new pane was opened with the file
let pane_group = pane_group_view(app, window_id, 0);
pane_group.read(app, |pane_group, _ctx| {
async_assert_eq!(
pane_group.pane_count(),
2,
"Expected 2 panes after opening file (terminal + editor)"
)
})
}),
)
.with_step(
new_step_with_default_assertions("Verify file opened in Warp editor").add_assertion(
assert_pane_title(0, 1, Regex::new(r"test_file\.txt$").unwrap()),
),
)
}
/// Test that the "Open in new pane" context menu action works correctly.
pub fn test_file_tree_open_in_new_pane() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
std::fs::write(
test_dir.join("sample.md"),
"# Sample Markdown\n\nThis is a test.",
)
.expect("Failed to create sample file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
new_step_with_default_assertions(
"Right-click on sample.md and select 'Open in new pane'",
)
.with_right_click_on_saved_position("file_tree_item:sample.md")
.with_click_on_saved_position("Open in new pane"),
)
.with_step(
new_step_with_default_assertions("Verify file opened in new pane")
.add_assertion(assert_pane_title(0, 1, Regex::new(r"sample\.md$").unwrap()))
.add_assertion(|app, window_id| {
let pane_group = pane_group_view(app, window_id, 0);
pane_group.read(app, |pane_group, _ctx| {
async_assert_eq!(
pane_group.pane_count(),
2,
"Expected 2 panes after 'Open in new pane'"
)
})
}),
)
}
/// Test that the "Open in new tab" context menu action works correctly.
pub fn test_file_tree_open_in_new_tab() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
std::fs::write(test_dir.join("config.json"), "{\"key\": \"value\"}")
.expect("Failed to create config file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
TestStep::new("Right-click on config.json and select 'Open in new tab'")
.with_right_click_on_saved_position("file_tree_item:config.json")
.with_click_on_saved_position("Open in new tab"),
)
.with_step(
TestStep::new("Verify file opened in new tab")
.add_assertion(|app, window_id| {
let workspace = workspace_view(app, window_id);
let tab_count = workspace.read(app, |workspace, _ctx| workspace.tab_count());
async_assert_eq!(tab_count, 2, "Expected 2 tabs after 'Open in new tab'")
})
.add_assertion(|app, window_id| {
let workspace = workspace_view(app, window_id);
let tab_count = workspace.read(app, |workspace, _ctx| workspace.tab_count());
let config_regex = Regex::new(r"config\.json$").unwrap();
let mut found = false;
for tab_index in 0..tab_count {
let pane_group = pane_group_view(app, window_id, tab_index);
let title = pane_group.read(app, |pane_group, ctx| {
pane_group.pane_by_index(0).map(|pane| {
pane.pane_configuration().as_ref(ctx).title().to_owned()
})
});
if let Some(title) = title {
if config_regex.is_match(&title) {
found = true;
break;
}
}
}
async_assert!(found, "Expected a tab with config.json opened")
}),
)
}
/// Test that keyboard navigation (arrow keys + enter) works to open files.
pub fn test_file_tree_keyboard_navigation() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
std::fs::create_dir_all(test_dir.join("src")).expect("Failed to create src directory");
std::fs::write(test_dir.join("src/file_a.txt"), "File A")
.expect("Failed to create file A");
std::fs::write(test_dir.join("src/file_b.txt"), "File B")
.expect("Failed to create file B");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
new_step_with_default_assertions("Focus file tree")
.with_click_on_saved_position("file_tree_item:src"),
)
.with_step(
new_step_with_default_assertions("Navigate to a file and press Enter")
.with_keystrokes(&["down", "enter"])
.add_assertion(|app, window_id| {
let pane_group = pane_group_view(app, window_id, 0);
pane_group.read(app, |pane_group, _ctx| {
async_assert_eq!(
pane_group.pane_count(),
2,
"Expected 2 panes after opening file via keyboard"
)
})
}),
)
}
/// Test that non-text files (like images) do not crash when clicked.
/// They should either open in the system default app or show an error.
pub fn test_file_tree_non_openable_files() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
// Create a binary file that shouldn't be opened in Warp
std::fs::write(test_dir.join("test.bin"), vec![0u8, 1, 2, 3, 255])
.expect("Failed to create binary file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
new_step_with_default_assertions("Click on binary file")
.with_click_on_saved_position("file_tree_item:test.bin")
.add_assertion(|app, window_id| {
// The binary file should NOT open in a new pane in Warp
// It should fall back to system default behavior
let pane_group = pane_group_view(app, window_id, 0);
pane_group.read(app, |pane_group, _ctx| {
async_assert_eq!(
pane_group.pane_count(),
1,
"Binary file should not open in Warp, should stay at 1 pane"
)
})
}),
)
}
/// Test that expanding directories and then clicking files inside them works correctly.
pub fn test_file_tree_nested_file_opening() -> Builder {
new_builder()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
// Create nested directory structure
std::fs::create_dir_all(test_dir.join("src/utils"))
.expect("Failed to create nested directories");
std::fs::write(
test_dir.join("src/utils/helper.js"),
"export function helper() { return 42; }",
)
.expect("Failed to create nested file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
new_step_with_default_assertions("Expand src directory")
.with_click_on_saved_position("file_tree_item:src"),
)
.with_step(
new_step_with_default_assertions("Expand utils directory")
.with_click_on_saved_position("file_tree_item:utils"),
)
.with_step(
new_step_with_default_assertions("Click on helper.js")
.with_click_on_saved_position("file_tree_item:helper.js")
.add_assertion(assert_pane_title(0, 1, Regex::new(r"helper\.js$").unwrap())),
)
}
+142
View File
@@ -0,0 +1,142 @@
use super::{new_builder, Builder};
use regex::Regex;
use warp::{
integration_testing::{
goto_line::{
assert_cursor_at_line, assert_cursor_at_line_and_column,
assert_goto_line_dialog_is_open, goto_line_confirm, open_goto_line_dialog,
},
step::new_step_with_default_assertions,
tab::assert_pane_title,
terminal::wait_until_bootstrapped_single_pane_for_tab,
view_getters::{pane_group_view, workspace_view},
},
workspace::WorkspaceAction,
};
use warpui::{async_assert_eq, App};
use crate::util::write_all_rc_files_for_test;
fn open_file_tree_panel(app: &mut App) {
let window_id = app.read(|ctx| {
ctx.windows()
.active_window()
.expect("should have active window")
});
let workspace = workspace_view(app, window_id);
app.update(|ctx| {
ctx.dispatch_typed_action_for_view(
window_id,
workspace.id(),
&WorkspaceAction::ToggleProjectExplorer,
);
});
}
fn create_multiline_test_file_content() -> String {
(1..=20)
.map(|i| format!("line {i} content"))
.collect::<Vec<_>>()
.join("\n")
}
fn file_open_steps(builder: Builder) -> Builder {
builder
.with_setup(|utils| {
let test_dir = utils.test_dir();
let dir_string = test_dir
.to_str()
.expect("Should be able to convert test dir to str");
write_all_rc_files_for_test(&test_dir, format!("cd {dir_string}"));
std::fs::write(
test_dir.join("goto_test.txt"),
create_multiline_test_file_content(),
)
.expect("Failed to create test file");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open file tree panel")
.with_action(|app, _, _| open_file_tree_panel(app)),
)
.with_step(
new_step_with_default_assertions("Click on goto_test.txt in file tree")
.with_click_on_saved_position("file_tree_item:goto_test.txt")
.add_assertion(|app, window_id| {
let pane_group = pane_group_view(app, window_id, 0);
pane_group.read(app, |pane_group, _ctx| {
async_assert_eq!(
pane_group.pane_count(),
2,
"Expected 2 panes after opening file"
)
})
}),
)
.with_step(
new_step_with_default_assertions("Verify file opened in editor").add_assertion(
assert_pane_title(0, 1, Regex::new(r"goto_test\.txt$").unwrap()),
),
)
}
pub fn test_goto_line_dialog_open_close() -> Builder {
file_open_steps(new_builder())
.with_step(
new_step_with_default_assertions("Open Go to Line dialog")
.with_action(|app, window_id, _| open_goto_line_dialog(app, window_id))
.add_assertion(assert_goto_line_dialog_is_open(true)),
)
.with_step(
new_step_with_default_assertions("Close Go to Line dialog with escape")
.with_keystrokes(&["escape"])
.add_assertion(assert_goto_line_dialog_is_open(false)),
)
}
pub fn test_goto_line_jumps_to_line() -> Builder {
file_open_steps(new_builder())
.with_step(
new_step_with_default_assertions("Open Go to Line dialog")
.with_action(|app, window_id, _| open_goto_line_dialog(app, window_id))
.add_assertion(assert_goto_line_dialog_is_open(true)),
)
.with_step(
new_step_with_default_assertions("Type line number and confirm")
.with_typed_characters(&["10"])
.with_keystrokes(&["enter"]),
)
.with_step(
new_step_with_default_assertions("Verify cursor at line 10")
.add_assertion(assert_goto_line_dialog_is_open(false))
.add_assertion(assert_cursor_at_line(10)),
)
}
pub fn test_goto_line_with_column() -> Builder {
file_open_steps(new_builder())
.with_step(
new_step_with_default_assertions("Go to line 5, column 3")
.with_action(|app, window_id, _| goto_line_confirm(app, window_id, "5:3")),
)
.with_step(
new_step_with_default_assertions("Verify cursor at line 5, column 3")
.add_assertion(assert_goto_line_dialog_is_open(false))
.add_assertion(assert_cursor_at_line_and_column(5, 3)),
)
}
pub fn test_goto_line_clamps_out_of_range() -> Builder {
file_open_steps(new_builder())
.with_step(
new_step_with_default_assertions("Go to line 999 (beyond file)")
.with_action(|app, window_id, _| goto_line_confirm(app, window_id, "999")),
)
.with_step(
new_step_with_default_assertions("Verify cursor clamped to last line")
.add_assertion(assert_goto_line_dialog_is_open(false))
.add_assertion(assert_cursor_at_line(20)),
)
}
+408
View File
@@ -0,0 +1,408 @@
use std::collections::HashMap;
use crate::Builder;
use settings::Setting as _;
use warp::{
integration_testing::{
self,
command_search::{assert_command_search_is_open, assert_history_filter_is_active},
input::assert_workflow_info_box_is_open,
step::new_step_with_default_assertions,
terminal::{assert_input_editor_contents, wait_until_bootstrapped_single_pane_for_tab},
view_getters::single_input_view,
},
search::command_search::settings::ShowGlobalWorkflowsInUniversalSearch,
sqlite_testing::set_user_and_hostname_for_commands,
terminal::{input::Input, model::session::get_local_hostname, shell::ShellType},
};
use warpui::{async_assert, ViewHandle};
use crate::util::{get_local_user, write_histfiles_for_test};
use super::{new_builder, TEST_ONLY_ASSETS};
/// The `history_with_metadata.sqlite` table looks like the following:
///
/// |id|command |exit_code|start_ts |completed_ts |pwd |shell|username |hostname |session_id |git_branch|cloud_workflow_id|workflow_command |
/// |1 |echo "foo" |0 |2023-07-11 16:29:32.092176|2023-07-11 16:29:33.124078|/Users/user|zsh |local:user |local:host |168911816423351|NULL |NULL |NULL |
/// |2 |[[ -n "foo" ]] |0 |2023-07-11 16:29:34.837961|2023-07-11 16:29:33.124078|/Users/user|zsh |local:user |local:host |168911816423351|NULL |NULL |[[ -n {{string}} ]]|
/// |3 |echo "bar" |0 |2023-07-11 16:29:42.000000|2023-07-11 16:29:43.000000|/Users/user|zsh |local:user |local:host |168911816423351|NULL |NULL |NULL |
/// |10|sed -i '' '/hello/d' foo|0 |2023-07-12 16:29:42.000000|2023-07-12 16:29:43.000000|/Users/user|zsh |local:user |local:host |168911816423351|NULL |NULL |sed -i '' '/{{string}}/d' {{file}}|
///
/// These three rows are duplicated in the table for each `shell` type so history tests can run on all shells with the same data.
///
/// Note that the user and host columns are updated at runtime in test setup to the actual local
/// user and host values for the machine on which this test is running -- this is necessary because
/// app logic depends on user and host values to match persisted commands to live sessions.
const FAKE_HISTORY_SQLITE_FILE: &str = "history_with_metadata.sqlite";
pub fn test_up_arrow_history() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(new_step_with_default_assertions("Run ls").with_keystrokes(&["l", "s", "enter"]))
.with_step(
new_step_with_default_assertions(
"Run multiline command and verify history menu is not visible",
)
.with_keystrokes(&["c", "shift-enter", "n", "enter"])
.add_assertion(|app, window_id| {
let views = app.views_of_type(window_id).unwrap();
let input_view: &ViewHandle<Input> = views.first().unwrap();
input_view.read(app, |view, ctx| {
async_assert!(
!view
.suggestions_mode_model()
.as_ref(ctx)
.mode()
.is_visible(),
"Input suggestion should not be visible right now."
)
})
}),
)
.with_step(
new_step_with_default_assertions(
"Enter up key and verify previous command is in buffer",
)
.with_keystrokes(&[
"up", // this should open the history menu
])
.add_assertion(|app, window_id| {
let views = app.views_of_type(window_id).unwrap();
let input_view: &ViewHandle<Input> = views.first().unwrap();
input_view.read(app, |view, ctx| {
// The history menu should be visible.
assert!(view
.suggestions_mode_model()
.as_ref(ctx)
.mode()
.is_visible());
// The cursor should be on the last row.
assert!(view.editor().as_ref(ctx).single_cursor_on_last_row(ctx));
async_assert!(
view.buffer_text(ctx) == *"c\nn",
"History menu should show the previous multiline command"
)
})
}),
)
.with_step(
new_step_with_default_assertions(
"Enter up key again and verify cursor moves to the top row",
)
.with_keystrokes(&[
"up", // this should move the cursor to the top line
])
.add_assertion(|app, window_id| {
let views = app.views_of_type(window_id).unwrap();
let input_view: &ViewHandle<Input> = views.first().unwrap();
input_view.read(app, |view, ctx| {
// The history menu should be visible.
assert!(view
.suggestions_mode_model()
.as_ref(ctx)
.mode()
.is_visible());
// The cursor should be on the first row.
assert!(view.editor().as_ref(ctx).single_cursor_on_first_row(ctx));
async_assert!(
view.buffer_text(ctx) == *"c\nn",
"History menu should still show the previous multiline command"
)
})
}),
)
}
pub fn test_up_arrow_history_enters_shift_tab_for_workflow() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
FAKE_HISTORY_SQLITE_FILE,
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_commands(local_user, local_hostname);
let home_dir = utils.test_dir();
write_histfiles_for_test(
home_dir,
vec![r#"echo "foo""#, r#"sed -i '' '/hello/d' foo"#],
[
ShellType::Zsh,
ShellType::Bash,
ShellType::Fish,
ShellType::PowerShell,
],
);
})
.with_user_defaults(HashMap::from([(
ShowGlobalWorkflowsInUniversalSearch::storage_key().to_owned(),
"true".to_owned(),
)]))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions(
"Enter up key and verify terminal input contains workflow command",
)
.with_keystrokes(&[
"up", // this should move the cursor to the top line
])
.add_assertion(|app, window_id| {
let input_view = single_input_view(app, window_id);
input_view.read(app, |view, ctx| {
// The history menu should be visible.
async_assert!(view
.suggestions_mode_model()
.as_ref(ctx)
.mode()
.is_visible())
})
})
.add_named_assertion(
"Input contains most recent command",
assert_input_editor_contents(0, "sed -i '' '/hello/d' foo"),
),
)
.with_step(
new_step_with_default_assertions("Update \"string\" workflow parameter")
.with_keystrokes(&[
"shift-tab", // this should cause the first argument to be highlighted
])
.with_typed_characters(&[
"bye", // this should result in us replacing the first argument
])
.add_named_assertion(
"First workflow parameter is substituted",
assert_input_editor_contents(0, "sed -i '' '/bye/d' foo"),
),
)
.with_step(
new_step_with_default_assertions("Update \"string\" workflow parameter")
.with_keystrokes(&["shift-tab"])
.with_typed_characters(&["baz"])
.add_named_assertion(
"Second workflow parameter is substituted",
assert_input_editor_contents(0, "sed -i '' '/bye/d' baz"),
),
)
}
/// Tests that history commands are loaded from the shell's histfile.
pub fn test_command_search_loads_history() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
FAKE_HISTORY_SQLITE_FILE,
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_commands(local_user, local_hostname);
let home_dir = utils.test_dir();
write_histfiles_for_test(
home_dir,
vec![r#"echo "foo""#, r#"[[ -n "foo" ]]"#, r#"echo "bar""#],
[
ShellType::Zsh,
ShellType::Bash,
ShellType::Fish,
ShellType::PowerShell,
],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open command search")
.with_keystrokes(&["ctrl-r"])
.add_named_assertion("Command search is open", assert_command_search_is_open()),
)
.with_step(
new_step_with_default_assertions("Select history filter")
.with_typed_characters(&["h"])
.with_keystrokes(&["tab"])
.add_named_assertion(
"History filter is active",
assert_history_filter_is_active(),
),
)
.with_step(
new_step_with_default_assertions("Loads history from sqlite")
.with_keystrokes(&["up", "up", "enter"])
.add_named_assertion(
"Input contains selected history command",
assert_input_editor_contents(0, r#"echo "foo""#),
),
)
}
/// Tests that history commands are loaded from the shell's histfile.
pub fn test_command_search_loads_history_from_nondefault_histfile_path() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
FAKE_HISTORY_SQLITE_FILE,
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_commands(local_user, local_hostname);
let base_dirs =
directories::BaseDirs::new().expect("should be able to determine home directory");
write_histfiles_for_test(
base_dirs.home_dir(),
vec![r#"echo "foo""#, r#"[[ -n "foo" ]]"#, r#"echo "bar""#],
[ShellType::Zsh, ShellType::Bash, ShellType::Fish],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open command search")
.with_keystrokes(&["ctrl-r"])
.add_named_assertion("Command search is open", assert_command_search_is_open()),
)
.with_step(
new_step_with_default_assertions("Select history filter")
.with_typed_characters(&["h"])
.with_keystrokes(&["tab"])
.add_named_assertion(
"History filter is active",
assert_history_filter_is_active(),
),
)
.with_step(
new_step_with_default_assertions("Loads history from sqlite")
.with_keystrokes(&["up", "up", "enter"])
.add_named_assertion(
"Input contains selected history command",
assert_input_editor_contents(0, r#"echo "foo""#),
),
)
}
/// Tests that commands in the histfile are treated as the "source of truth" for shell history, and
/// that the command rows persisted to the sqlite table are only used to join against the list of
/// histfile commands, effectively "enriching" them with metadata.
///
/// Basically, if a user manually deletes a command from their shell histfile, it should not show
/// up in Warp -- so we effectively do a "left join" on commands from the histfile with commands
/// loaded from sqlite.
pub fn test_histfile_left_joined_with_persisted_history() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
FAKE_HISTORY_SQLITE_FILE,
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_commands(local_user, local_hostname);
let home_dir = utils.test_dir();
write_histfiles_for_test(
home_dir,
vec![r#"echo "foo""#, r#"echo "bar""#],
[
ShellType::Zsh,
ShellType::Bash,
ShellType::Fish,
ShellType::PowerShell,
],
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open command search")
.with_keystrokes(&["ctrl-r"])
.add_named_assertion("Command search is open", assert_command_search_is_open()),
)
.with_step(
new_step_with_default_assertions("Select history filter")
.with_typed_characters(&["h"])
.with_keystrokes(&["tab"])
.add_named_assertion(
"History filter is active",
assert_history_filter_is_active(),
),
)
.with_step(
new_step_with_default_assertions("Loads history from sqlite")
.with_keystrokes(&["up", "enter"])
.add_named_assertion(
"Input contains history command from histfile",
assert_input_editor_contents(0, r#"echo "foo""#),
),
)
}
pub fn test_history_command_is_linked_to_local_workflow() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
FAKE_HISTORY_SQLITE_FILE,
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_commands(local_user, local_hostname);
let home_dir = utils.test_dir();
write_histfiles_for_test(
home_dir,
vec![r#"echo "foo""#, r#"[[ -n "foo" ]]"#, r#"echo "bar""#],
[
ShellType::Zsh,
ShellType::Bash,
ShellType::Fish,
ShellType::PowerShell,
],
);
})
.with_user_defaults(HashMap::from([(
ShowGlobalWorkflowsInUniversalSearch::storage_key().to_owned(),
"true".to_owned(),
)]))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open command search")
.with_keystrokes(&["ctrl-r"])
.add_named_assertion("Command search is open", assert_command_search_is_open()),
)
.with_step(
new_step_with_default_assertions("Select history filter")
.with_typed_characters(&["h"])
.with_keystrokes(&["tab"])
.add_named_assertion(
"History filter is active",
assert_history_filter_is_active(),
),
)
.with_step(
new_step_with_default_assertions("Loads history from sqlite")
.with_keystrokes(&["up", "enter"])
.add_named_assertion(
"Input contains selected history command",
assert_input_editor_contents(0, r#"[[ -n "foo" ]]"#),
)
.add_named_assertion(
"Workflows info box is open",
assert_workflow_info_box_is_open(0, 0),
),
)
}
+225
View File
@@ -0,0 +1,225 @@
use std::time::Duration;
use warp::integration_testing::terminal::util::current_shell_starter_and_version;
use warp::terminal::shell::ShellType;
use warp::{
features::FeatureFlag,
integration_testing::{
clipboard::write_to_clipboard,
input::{
assert_autosuggestion_state, input_contains_string, input_is_empty,
latest_buffer_operations_are_empty, tab_completions_menu_is_open, AutosuggestionState,
},
step::new_step_with_default_assertions,
terminal::{
execute_command_for_single_terminal_in_tab, util::ExpectedExitStatus,
wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::{single_input_view_for_tab, single_terminal_view_for_tab},
},
};
use warpui::{async_assert_eq, integration::TestStep, Event};
use crate::Builder;
use super::new_builder;
/// Ensures that tab completions are hidden when the completions menu is opened
/// but re-appear when the menu is closed.
pub fn test_autosuggestions_are_hidden_when_opening_tab_completions() -> Builder {
FeatureFlag::RemoveAutosuggestionDuringTabCompletions.set_enabled(true);
new_builder()
// Ensure that $HOME contains a directory as a tab-completion candidate.
.with_setup(|utils| {
let dir = utils.test_dir();
std::fs::create_dir(dir.join("foo")).expect("must be able to create dirs for test");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Execute a command so that we can generate autosuggestions.
.with_step(execute_command_for_single_terminal_in_tab(
0,
"cd .".into(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Insert 'cd' into input")
.with_typed_characters(&["cd "])
.add_named_assertion(
"Ensure cd is in input",
input_contains_string(0, String::from("cd ")),
)
.add_named_assertion(
"Ensure autosuggestion is present",
assert_autosuggestion_state(
0,
AutosuggestionState::ActiveWithText(String::from(".")),
),
),
)
.with_step(
new_step_with_default_assertions("Open tab completions menu")
.with_keystrokes(&["tab"])
.add_named_assertion(
"Ensure tab completions menu is open",
tab_completions_menu_is_open(0, true),
)
.add_named_assertion(
"Ensure autosuggestion is closed",
assert_autosuggestion_state(0, AutosuggestionState::Closed),
),
)
.with_step(
new_step_with_default_assertions("Close tab completions menu")
.with_keystrokes(&["escape"])
.add_named_assertion(
"Ensure tab completions menu is closed",
tab_completions_menu_is_open(0, false),
)
.add_named_assertion(
"Ensure autosuggestion is closed",
assert_autosuggestion_state(
0,
AutosuggestionState::ActiveWithText(String::from(".")),
),
),
)
}
pub fn test_latest_buffer_operations() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Execute a command so that we can generate autosuggestions.
.with_step(execute_command_for_single_terminal_in_tab(
0,
"cd .".into(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Check initial state").add_named_assertion(
"Ensure the latest buffer operations start off empty",
latest_buffer_operations_are_empty(0, true),
),
)
.with_step(
new_step_with_default_assertions("Write into the input")
.with_typed_characters(&["echo 'foo'"])
.add_named_assertion(
"Ensure the input was written to",
input_contains_string(0, String::from("echo 'foo'")),
)
.add_named_assertion(
"Ensure the latest buffer operations are non-empty",
latest_buffer_operations_are_empty(0, false),
),
)
.with_step(
new_step_with_default_assertions("Run the command with the current buffer text")
.with_keystrokes(&["enter"])
.add_named_assertion("Ensure the input is empty", input_is_empty(0))
.add_named_assertion(
"Ensure the latest buffer operations are empty",
latest_buffer_operations_are_empty(0, true),
),
)
}
pub fn test_middle_click_paste() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(write_to_clipboard(String::from("abc")).add_named_assertion(
"Ensure the input is empty to start",
input_contains_string(0, String::from("")),
))
.with_step(
TestStep::new("Middle click in the input editor")
.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(view.prompt_save_position_id())
.expect("prompt should have a position")
.origin();
// Move the position slightly so it's clearly over the editor.
position.set_x(position.x() + 10.);
position.set_y(position.y() + 5.);
Event::MiddleMouseDown {
position,
cmd: false,
shift: false,
click_count: 1,
}
})
})
.add_named_assertion(
"Ensure the text is pasted once",
input_contains_string(0, String::from("abc")),
),
)
.with_step(
TestStep::new("Middle click on the prompt area")
.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(view.prompt_save_position_id())
.expect("prompt should have a position")
.origin();
// Move the position slightly so it's clearly over the prompt.
position.set_x(position.x() + 10.);
position.set_y(position.y() + 5.);
Event::MiddleMouseDown {
position,
cmd: false,
shift: false,
click_count: 1,
}
})
})
.add_named_assertion(
"Ensure the text is pasted again",
input_contains_string(0, String::from("abcabc")),
),
)
}
/// Checks that the git branch prompt chip value is correctly populated.
pub fn test_git_prompt_chips() -> Builder {
// 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()
.set_should_run_test(|| {
// TODO(alokedesai): Re-enable for Powershell once the cause of the flakiness has been
// resolved.
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.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,
"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(
new_step_with_default_assertions("Git branch chip should be populated").set_timeout(Duration::from_secs(15)).add_assertion(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |terminal_view, ctx| {
terminal_view.input().read(ctx, |input_view, ctx| {
let git_branch = input_view.prompt_render_helper.git_branch(ctx);
async_assert_eq!(git_branch, Some("main".to_string()))
})
})
}),
)
}
@@ -0,0 +1,626 @@
use std::time::Duration;
use warp::features::FeatureFlag;
use warp::integration_testing::{
step::new_step_with_default_assertions,
terminal::{
assert_long_running_block_executing_for_single_terminal_in_tab,
wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::single_terminal_view_for_tab,
};
use warpui::event::{KeyEventDetails, KeyState};
use warpui::keymap::Keystroke;
use warpui::platform::keyboard::KeyCode;
use warpui::{async_assert, integration::TestStep, Event};
use crate::Builder;
use super::new_builder;
/// Helper: creates a setup closure that writes a Python script asset to the test directory.
macro_rules! setup_python_script {
($filename:expr, $asset_path:expr) => {
|utils| {
let script_path = utils.test_dir().join($filename);
let script_content = include_bytes!($asset_path);
std::fs::write(&script_path, script_content).expect("Failed to write test script");
}
};
}
/// Helper: creates a step that waits for "Protocol enabled" to appear in terminal output.
fn wait_for_protocol_enabled() -> TestStep {
TestStep::new("Wait for protocol to be enabled")
.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 output = model.block_list().active_block().output_to_string();
async_assert!(
output.contains("Protocol enabled"),
"Protocol should be enabled, but output was: {output}"
)
})
})
.set_timeout(Duration::from_secs(5))
}
/// Helper: creates an assertion closure that checks the terminal output contains `expected`.
fn assert_output_contains(
expected: &'static str,
description: &'static str,
) -> impl FnMut(&mut warpui::App, warpui::WindowId) -> warpui::integration::AssertionOutcome {
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 output = model.block_list().active_block().output_to_string();
async_assert!(
output.contains(expected),
"{description}, but output was: {output}"
)
})
}
}
/// Test that without keyboard protocol enabled, Shift+Enter sends \n
pub fn test_keyboard_protocol_disabled_shift_enter() -> Builder {
new_builder()
.with_setup(setup_python_script!(
"read_keys.py",
"../../assets/read_keys.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys.py")
.with_typed_characters(&["python3 ~/read_keys.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(
TestStep::new("Wait for script to be ready")
.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 output = model.block_list().active_block().output_to_string();
// Wait until the "Ready" message appears
async_assert!(
output.contains("Ready"),
"Script should be ready, but output was: {}",
output
)
})
})
.set_timeout(Duration::from_secs(5)),
)
.with_step(
TestStep::new("Send Shift+Enter")
.with_keystrokes(&["shift-enter"])
.set_timeout(Duration::from_secs(5))
.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 output = model.block_list().active_block().output_to_string();
// Should see "Received byte: 0x0a" in the output (newline)
async_assert!(
output.contains("0x0a"),
"Expected Shift+Enter to send 0x0a (\\n), but output was: {}",
output
)
})
}),
)
.with_step(
TestStep::new("Send plain Enter")
.with_keystrokes(&["enter"])
.set_timeout(Duration::from_secs(5))
.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 output = model.block_list().active_block().output_to_string();
// Should see "Received byte: 0x0d" in the output (carriage return)
async_assert!(
output.contains("0x0d"),
"Expected plain Enter to send 0x0d (\\r), but output was: {}",
output
)
})
}),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test that when keyboard protocol is enabled, Shift+Enter sends CSI u sequence
pub fn test_keyboard_protocol_enabled_shift_enter() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_with_protocol.py",
"../../assets/read_keys_with_protocol.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_with_protocol.py")
.with_typed_characters(&["python3 ~/read_keys_with_protocol.py"])
.with_keystrokes(&["enter"])
.add_assertion(assert_long_running_block_executing_for_single_terminal_in_tab(true, 0)),
)
.with_step(wait_for_protocol_enabled())
.with_step(
TestStep::new("Send Shift+Enter")
.with_keystrokes(&["shift-enter"])
.set_timeout(Duration::from_secs(5))
.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 output = model
.block_list()
.active_block()
.output_to_string();
// Should see CSI sequence like '\x1b[13;2u'
// which is ESC [ 13 ; 2 u (Enter=13, Shift=2)
async_assert!(
output.contains("13;2u"),
"Expected Shift+Enter to send CSI u sequence (ESC [ 13 ; 2 u), but output was: {}",
output
)
})
}),
)
.with_step(
TestStep::new("Send plain Enter")
.with_keystrokes(&["enter"])
.set_timeout(Duration::from_secs(5))
.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 output = model
.block_list()
.active_block()
.output_to_string();
// Plain Enter with no modifiers sends CSI u sequence.
// Per Kitty protocol, this can be either:
// - ESC [ 13 u (modifier omitted when no modifiers)
// - ESC [ 13 ; 1 u (explicit modifier=1)
// Check for the "Complete sequence" repr output to avoid
// matching the earlier Shift+Enter "13;2u" substring.
async_assert!(
output.contains("'\\x1b[13u'") || output.contains("'\\x1b[13;1u'"),
"Expected plain Enter to send CSI u sequence (ESC [ 13 u or ESC [ 13 ; 1 u), but output was: {}",
output
)
})
}),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit")
.with_keystrokes(&["ctrl-c"]),
)
}
/// Test that shifted printable keys encode the unshifted keycode in CSI-u.
pub fn test_keyboard_protocol_enabled_shifted_symbol_uses_unshifted_keycode() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_with_protocol.py",
"../../assets/read_keys_with_protocol.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_with_protocol.py")
.with_typed_characters(&["python3 ~/read_keys_with_protocol.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
// Shift+@ → 50;2u (base key '2'=50 with Shift modifier=2)
.with_step(
TestStep::new("Send Shift+@ with key_without_modifiers='2'")
.with_event(Event::KeyDown {
keystroke: Keystroke::parse("shift-@").unwrap(),
chars: "@".to_string(),
details: KeyEventDetails {
key_without_modifiers: Some("2".to_string()),
..Default::default()
},
is_composing: false,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"50;2u",
"Expected Shift+@ to encode as 50;2u (base key '2'=50)",
)),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test alternate keys (flag 4) and associated text (flag 16) encoding.
/// With flags 29 (1+4+8+16), shift+A should produce CSI 97:65;2;65u
/// (base=97 'a', alternate=65 'A', shift modifier=2, text=65 'A').
pub fn test_keyboard_protocol_alternate_keys_and_text() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_alternate_text.py",
"../../assets/read_keys_alternate_text.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_alternate_text.py")
.with_typed_characters(&["python3 ~/read_keys_alternate_text.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
.with_step(
// Shift+A should produce: CSI 97:65;2;65u
// - 97 = base key 'a' (unshifted)
// - :65 = alternate key 'A' (shifted) from flag 4
// - ;2 = shift modifier
// - ;65 = associated text 'A' from flag 16
TestStep::new("Send Shift+A")
.with_keystrokes(&["shift-A"])
.set_timeout(Duration::from_secs(5))
.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 output = model.block_list().active_block().output_to_string();
async_assert!(
output.contains("97:65;2;65u"),
"Expected Shift+A to produce CSI 97:65;2;65u (alternate + text), but output was: {}",
output
)
})
}),
)
.with_step(
// Plain 'a' should produce: CSI 97;1;97u
// - 97 = key 'a'
// - ;1 = no modifiers (must be present because text field follows)
// - ;97 = associated text 'a' from flag 16
// No alternate key because shift is not held.
TestStep::new("Send plain 'a'")
.with_keystrokes(&["a"])
.set_timeout(Duration::from_secs(5))
.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 output = model.block_list().active_block().output_to_string();
async_assert!(
output.contains("97;1;97u"),
"Expected plain 'a' to produce CSI 97;1;97u (with associated text), but output was: {}",
output
)
})
}),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit")
.with_keystrokes(&["ctrl-c"]),
)
}
/// Test kitty apply-mode semantics and query responses through terminal integration.
pub fn test_keyboard_protocol_query_and_apply_modes() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"query_keyboard_modes.py",
"../../assets/query_keyboard_modes.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute query_keyboard_modes.py")
.with_typed_characters(&["python3 ~/query_keyboard_modes.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(
TestStep::new("Verify query responses")
.set_timeout(Duration::from_secs(15))
.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 output = model.block_list().active_block().output_to_string();
// After set=1 → query should give ?1u
// After union 8 (onto 1) → query should give ?9u
// After diff 1 (from 9) → query should give ?8u
async_assert!(
output.contains("query_1=b'\\x1b[?1u'")
&& output.contains("query_2=b'\\x1b[?9u'")
&& output.contains("query_3=b'\\x1b[?8u'"),
"Expected query/apply responses (query_1=?1u, query_2=?9u, query_3=?8u), but output was: {}",
output
)
})
}),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test flag 8 (report all keys as escape codes): printable chars become CSI u,
/// cursor keys remain legacy, and Ctrl+key combos include modifier.
pub fn test_keyboard_protocol_report_all_keys_printable_and_cursor() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_report_all.py",
"../../assets/read_keys_report_all.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_report_all.py")
.with_typed_characters(&["python3 ~/read_keys_report_all.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
// Plain 'a' → ESC[97u
.with_step(
TestStep::new("Send plain 'a'")
.with_keystrokes(&["a"])
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"97u",
"Expected plain 'a' to send CSI u with key code 97",
)),
)
// Plain '1' → ESC[49u
.with_step(
TestStep::new("Send plain '1'")
.with_keystrokes(&["1"])
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"49u",
"Expected plain '1' to send CSI u with key code 49",
)),
)
// Up arrow → legacy ESC[A (cursor keys are not encoded via CSI u)
.with_step(
TestStep::new("Send Up arrow")
.with_keystrokes(&["up"])
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"Legacy arrow",
"Expected Up arrow to use legacy encoding (ESC[A), not CSI u",
)),
)
// Ctrl+a → ESC[97;5u
.with_step(
TestStep::new("Send Ctrl+a")
.with_keystrokes(&["ctrl-a"])
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"97;5u",
"Expected Ctrl+a to send CSI u with key code 97 and Ctrl modifier (97;5u)",
)),
)
// 'é' (U+00E9 = 233) → CSI 233u. Tests that multi-byte UTF-8 characters
// are correctly handled via `key.chars().count() == 1`.
.with_step(
TestStep::new("Send 'é' (U+00E9)")
.with_event(Event::KeyDown {
keystroke: Keystroke {
key: "é".to_string(),
ctrl: false,
alt: false,
shift: false,
cmd: false,
meta: false,
},
chars: "é".to_string(),
details: KeyEventDetails::default(),
is_composing: false,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"233u",
"Expected 'é' (U+00E9) to encode as CSI 233u",
)),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test flag 2 (report event types): with flags 1+2+8=11, press is the default event
/// type and is omitted per the Kitty spec. Pressing 'a' produces ESC[97u (same as
/// without flag 2). Event types only differ for repeat/release events.
pub fn test_keyboard_protocol_event_types() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_event_types.py",
"../../assets/read_keys_event_types.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_event_types.py")
.with_typed_characters(&["python3 ~/read_keys_event_types.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
// With flags 11 (1+2+8), pressing 'a' produces ESC[97u.
// Press is the default event type and is omitted.
.with_step(
TestStep::new("Send 'a' and verify event type encoding")
.with_keystrokes(&["a"])
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"97u",
"Expected 'a' to be encoded as CSI 97u (press event type is default, omitted)",
)),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test standalone modifier key reporting with flags 1+2+8=11.
/// Sends ModifierKeyChanged events for ShiftLeft press/release and verifies
/// the CSI u encoding includes the correct key code and event type.
pub fn test_keyboard_protocol_modifier_key_reporting() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_event_types.py",
"../../assets/read_keys_event_types.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_event_types.py")
.with_typed_characters(&["python3 ~/read_keys_event_types.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
// ShiftLeft press → ESC[57441;2:1u (key code 57441, modifiers=2 with self-bit, event_type=1 press)
.with_step(
TestStep::new("Send ShiftLeft press")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::ShiftLeft,
state: KeyState::Pressed,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57441;2:1u",
"Expected ShiftLeft press to produce CSI u with key code 57441, modifiers=2 (self-bit), event type :1",
)),
)
// ShiftLeft release → ESC[57441;2:3u (modifiers=2 with self-bit, event_type=3 release)
.with_step(
TestStep::new("Send ShiftLeft release")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::ShiftLeft,
state: KeyState::Released,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57441;2:3u",
"Expected ShiftLeft release to produce CSI u with modifiers=2 (self-bit) and event type :3",
)),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
/// Test that all modifier keys include the correct self-bit in their CSI u encoding.
/// Per the Kitty spec, pressing a modifier key alone must set its own modifier bit:
/// - ShiftLeft (57441): modifiers = 1 + shift(1) = 2
/// - ControlLeft (57442): modifiers = 1 + ctrl(4) = 5
/// - AltLeft (57443): modifiers = 1 + alt(2) = 3
///
/// Uses flags 1+2+8=11 to enable event type reporting.
pub fn test_keyboard_protocol_modifier_self_bit() -> Builder {
FeatureFlag::KittyKeyboardProtocol.set_enabled(true);
new_builder()
.with_setup(setup_python_script!(
"read_keys_event_types.py",
"../../assets/read_keys_event_types.py"
))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute read_keys_event_types.py")
.with_typed_characters(&["python3 ~/read_keys_event_types.py"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(wait_for_protocol_enabled())
// ControlLeft press → ESC[57442;5:1u (modifiers = 1 + ctrl(4) = 5)
.with_step(
TestStep::new("Send ControlLeft press")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::ControlLeft,
state: KeyState::Pressed,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57442;5:1u",
"Expected ControlLeft press to produce CSI u with modifiers=5 (self-bit ctrl=4)",
)),
)
// ControlLeft release → ESC[57442;5:3u
.with_step(
TestStep::new("Send ControlLeft release")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::ControlLeft,
state: KeyState::Released,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57442;5:3u",
"Expected ControlLeft release to produce CSI u with modifiers=5 and event type :3",
)),
)
// AltLeft press → ESC[57443;3:1u (modifiers = 1 + alt(2) = 3)
.with_step(
TestStep::new("Send AltLeft press")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::AltLeft,
state: KeyState::Pressed,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57443;3:1u",
"Expected AltLeft press to produce CSI u with modifiers=3 (self-bit alt=2)",
)),
)
// AltLeft release → ESC[57443;3:3u
.with_step(
TestStep::new("Send AltLeft release")
.with_event(Event::ModifierKeyChanged {
key_code: KeyCode::AltLeft,
state: KeyState::Released,
})
.set_timeout(Duration::from_secs(5))
.add_assertion(assert_output_contains(
"57443;3:3u",
"Expected AltLeft release to produce CSI u with modifiers=3 and event type :3",
)),
)
.with_step(
new_step_with_default_assertions("Send Ctrl+C to exit").with_keystrokes(&["ctrl-c"]),
)
}
@@ -0,0 +1,509 @@
use std::{path::PathBuf, time::Duration};
use warpui::{
async_assert,
integration::{AssertionOutcome, TestStep},
ModelHandle,
};
use super::{assert_approx_eq, new_builder, TEST_ONLY_ASSETS};
use crate::Builder;
use warp::integration_testing::{
pane_group::assert_focused_pane_index,
window::assert_num_windows_open,
workspace::{assert_focused_tab_index, assert_tab_count},
};
use warp::integration_testing::{
step::new_step_with_default_assertions,
terminal::{validate_block_output, wait_until_bootstrapped_single_pane_for_tab},
};
use warp::search::command_palette::launch_config;
use warp::workspace::NEW_TAB_BUTTON_POSITION_ID;
use warp::{features::FeatureFlag, integration_testing::settings::set_window_custom_size};
use warp::{
integration_testing::type_getters::get_launch_config_ui_location, search::SyncDataSource,
};
use warp::{
integration_testing::{self},
search::data_source::Query,
};
/// Adds a launch config to the mocked out warp config directory and verifies that
/// the launch config appears in the launch config palette.
pub fn test_add_launch_config_to_warp_config() -> Builder {
new_builder()
.with_setup(move |utils| {
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some((10).to_string()));
std::fs::create_dir_all(integration_testing::launch_configs::launch_configs_dir())
.expect("Should be able to create launch configs dir");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Launch config palette should be empty").add_named_assertion(
"Launch config palette should be empty",
|app, _| {
let launch_config_data_source: ModelHandle<launch_config::DataSource> = app
.models_of_type()
.first()
.expect("launch config must exist")
.clone();
launch_config_data_source.read(app, |palette, app| {
// Note that this can be a synchronous assertion because unlike the next test step,
// we don't have concurrency with a WarpConfig watcher thread
assert_eq!(
palette.run_query(&Query::from(""), app).unwrap().len(),
0,
"There should not be any launch configs in the palette"
);
});
AssertionOutcome::Success
},
),
)
.with_step(
TestStep::new("Write a new launch config")
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test_launch_config.yaml",
&integration_testing::launch_configs::launch_configs_dir()
.join("test_launch_config.yaml"),
);
})
.add_named_assertion(
"The added launch config should be in the palette",
|app, _| {
let launch_config_data_source: ModelHandle<launch_config::DataSource> = app
.models_of_type()
.first()
.expect("launch config must exist")
.clone();
let num_configs = launch_config_data_source.read(app, |palette, ctx| {
palette.run_query(&Query::from(""), ctx).unwrap().len()
});
async_assert!(
num_configs == 1,
"Expected to find one launch config, instead found {}",
num_configs
)
},
),
)
}
pub fn test_with_launch_config() -> 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(assert_num_windows_open(1)),
)
.with_step(
new_step_with_default_assertions("Opening a configuration template").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config:
warp::launch_configs::launch_config::make_mock_single_window_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
);
},
),
)
.with_step(
new_step_with_default_assertions("Assert the new window matches template")
.add_named_assertion("Created a new window", move |app, _| {
assert_eq!(app.window_ids().len(), 2);
AssertionOutcome::Success
})
.add_assertion(assert_tab_count(2))
.add_named_assertion("Validate first tab", move |app, window_id| {
validate_block_output("test_command", 0, 0, window_id, app)
})
.add_named_assertion("Validate second tab", move |app, window_id| {
validate_block_output("test_command_on_another_tab", 1, 0, window_id, app)
}),
)
}
// TODO(CORE-2300): Once we remove FeatureFlag::ShellSelector, we should remove this test.
pub fn test_open_launch_config_from_add_tab_menu_legacy() -> Builder {
new_builder()
.set_should_run_test(|| !FeatureFlag::ShellSelector.is_enabled())
.with_setup(move |utils| {
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some((10).to_string()));
// Write a new launch config file. Launch config is named "Launch Config"
let dir = integration_testing::launch_configs::launch_configs_dir();
std::fs::create_dir_all(&dir).expect("Should be able to create launch configs dir");
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test_launch_config.yaml",
&dir.join("test_launch_config.yaml"),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Right click on new tab button")
.with_right_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID),
)
.with_step(
new_step_with_default_assertions("Press Launch Config menu item")
// Since we only have one launch config, it should be the third menu item and the
// second one is disabled.
.with_keystrokes(&["down", "down", "enter"]),
)
.with_step(
new_step_with_default_assertions("Assert that three new windows are created")
.add_assertion(assert_num_windows_open(4)),
)
}
pub fn test_launch_config_single_child_branch() -> Builder {
use warp::launch_configs::launch_config::{
LaunchConfig, PaneMode, PaneTemplateType, SplitDirection, TabTemplate, WindowTemplate,
};
use warpui::actions::StandardAction;
/// Create a launch config that has a branch with a single child
fn create_launch_config() -> LaunchConfig {
LaunchConfig {
name: "Mocked config".to_owned(),
active_window_index: Some(0),
windows: vec![WindowTemplate {
active_tab_index: Some(0),
tabs: vec![TabTemplate {
title: Some("First tab".to_owned()),
layout: PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Horizontal,
panes: vec![PaneTemplateType::PaneTemplate {
is_focused: Some(true),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
}],
},
color: None,
}],
}],
}
}
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Opening a launch config with single child branch")
.with_action(move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config: create_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
);
}),
)
.with_step(
new_step_with_default_assertions("Close the open pane with standard action")
.add_assertion(|app, window_id| {
app.dispatch_standard_action(window_id, StandardAction::Close);
// If we get here without panicking, then we are successful
AssertionOutcome::Success
}),
)
}
pub fn test_open_launch_config_with_custom_size() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert we only have 1 window open at start")
.add_assertion(assert_num_windows_open(1)),
)
.with_step(set_window_custom_size(40, 20))
.with_step(
new_step_with_default_assertions("Open a launch configuration").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config:
warp::launch_configs::launch_config::make_mock_single_window_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
)
},
),
)
.with_step(
new_step_with_default_assertions("Assert the new window uses the custom size")
.add_named_assertion("Validate window size", move |app, window_id| {
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(), 192., epsilon = 2.);
assert_approx_eq!(f32, size.y(), 644., epsilon = 2.);
AssertionOutcome::Success
}),
)
}
pub fn test_open_launch_config_in_active_window() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert we only have 1 window, 1 tab open at start")
.add_assertion(assert_num_windows_open(1))
.add_assertion(assert_tab_count(1))
)
.with_step(
new_step_with_default_assertions("Open a launch configuration").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config:
warp::launch_configs::launch_config::make_mock_single_window_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: true,
},
)
},
)
// Add a post-step pause so that we can make sure any windows were opened
// in time, if they were going to be.
.set_post_step_pause(Duration::from_secs(1))
)
.with_step(
new_step_with_default_assertions("Assert we only have 1 window, 3 tabs (1 old, 2 new) after launching")
.add_assertion(assert_num_windows_open(1))
.add_assertion(assert_tab_count(3))
)
}
pub fn test_with_launch_config_with_active_tab_index() -> Builder {
use warp::launch_configs::launch_config::{
LaunchConfig, PaneMode, PaneTemplateType, SplitDirection, TabTemplate, WindowTemplate,
};
fn create_launch_config() -> LaunchConfig {
LaunchConfig {
name: "Mocked config".to_owned(),
active_window_index: Some(0),
windows: vec![WindowTemplate {
active_tab_index: Some(1),
tabs: vec![
TabTemplate {
title: None,
layout: PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Horizontal,
panes: vec![PaneTemplateType::PaneTemplate {
is_focused: Some(true),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
}],
},
color: None,
};
3
],
}],
}
}
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(assert_num_windows_open(1)),
)
.with_step(
new_step_with_default_assertions("Opening a configuration template").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config: create_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
);
},
),
)
.with_step(
new_step_with_default_assertions("Assert the new window matches template")
.add_assertion(assert_tab_count(3))
.add_assertion(assert_focused_tab_index(1)),
)
}
pub fn test_with_launch_config_with_active_pane() -> Builder {
use warp::launch_configs::launch_config::{
LaunchConfig, PaneMode, PaneTemplateType, SplitDirection, TabTemplate, WindowTemplate,
};
fn create_launch_config() -> LaunchConfig {
LaunchConfig {
name: "Mocked config".to_owned(),
active_window_index: Some(0),
windows: vec![WindowTemplate {
active_tab_index: Some(0),
tabs: vec![TabTemplate {
title: None,
layout: PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Horizontal,
panes: vec![
PaneTemplateType::PaneTemplate {
is_focused: Some(false),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Vertical,
panes: vec![
PaneTemplateType::PaneTemplate {
is_focused: Some(false),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
PaneTemplateType::PaneTemplate {
is_focused: Some(true),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
],
},
],
},
color: None,
}],
}],
}
}
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(assert_num_windows_open(1)),
)
.with_step(
new_step_with_default_assertions("Opening a configuration template").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config: create_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
);
},
),
)
.with_step(
new_step_with_default_assertions("Assert the bottom right pane is selected")
.add_assertion(assert_tab_count(1))
.add_assertion(assert_focused_tab_index(0))
.add_assertion(assert_focused_pane_index(0, 2)),
)
}
pub fn test_with_launch_config_with_no_active_pane() -> Builder {
use warp::launch_configs::launch_config::{
LaunchConfig, PaneMode, PaneTemplateType, SplitDirection, TabTemplate, WindowTemplate,
};
fn create_launch_config() -> LaunchConfig {
LaunchConfig {
name: "Mocked config".to_owned(),
active_window_index: Some(0),
windows: vec![WindowTemplate {
active_tab_index: Some(0),
tabs: vec![TabTemplate {
title: None,
layout: PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Horizontal,
panes: vec![
PaneTemplateType::PaneTemplate {
is_focused: Some(false),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
PaneTemplateType::PaneBranchTemplate {
split_direction: SplitDirection::Vertical,
panes: vec![
PaneTemplateType::PaneTemplate {
is_focused: Some(false),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
PaneTemplateType::PaneTemplate {
is_focused: Some(false),
cwd: PathBuf::from("/some/path"),
commands: Vec::new(),
pane_mode: PaneMode::Terminal,
shell: None,
},
],
},
],
},
color: None,
}],
}],
}
}
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(assert_num_windows_open(1)),
)
.with_step(
new_step_with_default_assertions("Opening a configuration template").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_launch_config",
warp::root_view::OpenLaunchConfigArg {
launch_config: create_launch_config(),
ui_location: get_launch_config_ui_location(),
open_in_active_window: false,
},
);
},
),
)
.with_step(
new_step_with_default_assertions("Assert the leftmost/topmost pane is focused")
.add_assertion(assert_tab_count(1))
.add_assertion(assert_focused_tab_index(0))
.add_assertion(assert_focused_pane_index(0, 0)),
)
}
+220
View File
@@ -0,0 +1,220 @@
use warp::{
cmd_or_ctrl_shift,
features::FeatureFlag,
integration_testing::{
command_palette::open_command_palette_and_run_action,
notebook::{
assert_notebook_contents, assert_notebook_id, assert_notebook_not_open,
assert_notebook_open, assert_notebook_renders_mermaid_diagram,
assert_open_in_warp_banner_open, create_a_personal_notebook,
enter_notebook_edit_mode_and_set_markdown, move_notebook_cursor_to_offset,
open_notebook,
},
step::new_step_with_default_assertions,
tab::{assert_pane_title, assert_tab_title},
terminal::{
assert_single_terminal_in_tab_bootstrapped, execute_command_for_single_terminal_in_tab,
util::ExpectedExitStatus, wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::terminal_view,
window::{add_and_save_window, close_window, save_active_window_id},
},
};
use warpui::integration::TestStep;
use super::{new_builder, Builder};
pub fn test_notebook_pane_tracking() -> Builder {
new_builder()
.with_step(
create_a_personal_notebook("the notebook", "A test notebook")
.add_assertion(save_active_window_id("first window")),
)
.with_step(
open_notebook("first window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open",
assert_notebook_id(0, 0, "the notebook"),
),
)
// Now, reopen the notebook (from both windows) and verify that it's only opened once.
.with_step(
open_notebook("first window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open once",
assert_notebook_open("the notebook"),
),
)
.with_step(add_and_save_window("second window"))
.with_step(
open_notebook("second window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open once",
assert_notebook_open("the notebook"),
),
)
// Close and then reopen the notebook.
.with_step(
TestStep::new("Close the open notebook")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is closed",
assert_notebook_not_open("the notebook"),
),
)
.with_step(open_notebook("second window", "the notebook"))
// This must be in a separate step so that the active window is updated.
.with_step(
TestStep::new("Verify notebook is open in second window")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open in the second window",
assert_notebook_id(0, 0, "the notebook"),
),
)
}
/// This is a regression test for CLD-713.
pub fn test_close_notebook_tab() -> Builder {
new_builder()
.with_step(
create_a_personal_notebook("the notebook", "Test Notebook")
.add_assertion(save_active_window_id("the window")),
)
.with_step(
open_notebook("the window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open",
assert_notebook_id(0, 0, "the notebook"),
),
)
.with_step(
TestStep::new("Open a tab with cmd-t")
.with_keystrokes(&[cmd_or_ctrl_shift("t")])
.add_assertion(|app, window_id| {
assert_single_terminal_in_tab_bootstrapped(app, window_id, 1)
}),
)
// Change the tab title so we can identify it.
.with_steps(open_command_palette_and_run_action(
"Rename the Current Tab",
))
.with_step(TestStep::new("Set tab title").with_input_string("tab2", Some(&["enter"])))
// Refocus the first notebook pane.
.with_step(
open_notebook("the window", "the notebook")
.add_assertion(assert_tab_title(0, "Test Notebook")),
)
// Close the first tab, and wait for the second to be focused.
.with_step(
TestStep::new("Close the notebook tab")
.with_hover_over_saved_position("close_tab_button:0")
.with_click_on_saved_position("close_tab_button:0")
.add_assertion(assert_tab_title(0, "tab2")),
)
.with_step(
open_notebook("the window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open",
assert_notebook_id(0, 0, "the notebook"),
),
)
}
pub fn test_close_notebook_window() -> Builder {
new_builder()
.with_step(
create_a_personal_notebook("the notebook", "Test Notebook")
.add_assertion(save_active_window_id("first window")),
)
.with_step(
open_notebook("first window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open",
assert_notebook_id(0, 0, "the notebook"),
),
)
.with_step(add_and_save_window("second window"))
// Close the first window
.with_step(
close_window("first window", 1).add_named_assertion_with_data_from_prior_step(
"Verify notebook is closed",
assert_notebook_not_open("the notebook"),
),
)
// Reopen the notebook in the remaining window.
.with_step(
open_notebook("second window", "the notebook")
.add_assertion(assert_tab_title(0, "Test Notebook")),
)
}
pub fn test_open_in_warp_banner() -> Builder {
new_builder()
.with_setup(|utils| {
std::fs::write(utils.test_dir().join("README.md"), "# Hello, world!")
.expect("Couldn't create README.md");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
execute_command_for_single_terminal_in_tab(
0,
"cat README.md".to_string(),
ExpectedExitStatus::Success,
(),
)
.add_assertion(assert_open_in_warp_banner_open(0, 0)),
)
.with_step(
new_step_with_default_assertions("Click Open in Warp banner")
.with_click_on_saved_position_fn(|app, window_id| {
let view = terminal_view(app, window_id, 0, 0);
format!("open_in_warp_banner_button_{}", view.id())
}),
)
.with_step(
new_step_with_default_assertions("Wait for Markdown file to open")
.add_assertion(assert_pane_title(0, 1, "README.md")),
)
}
pub fn test_backspace_inside_rendered_mermaid_block_is_atomic() -> Builder {
FeatureFlag::MarkdownMermaid.set_enabled(true);
FeatureFlag::EditableMarkdownMermaid.set_enabled(true);
let markdown = "Before\n```mermaid\ngraph TD\nA --> B\n```\nAfter";
let mermaid_block_start = markdown
.find("```mermaid")
.expect("Mermaid block should exist");
let cursor_offset = markdown
.find("graph TD")
.expect("Mermaid source should exist")
+ 3;
new_builder()
.with_step(
create_a_personal_notebook("the notebook", "Mermaid Notebook")
.add_assertion(save_active_window_id("the window")),
)
.with_step(
open_notebook("the window", "the notebook")
.add_named_assertion_with_data_from_prior_step(
"Verify notebook is open",
assert_notebook_id(0, 0, "the notebook"),
),
)
.with_step(
enter_notebook_edit_mode_and_set_markdown(0, 0, markdown)
.add_assertion(assert_notebook_contents(0, 0, markdown))
.add_assertion(assert_notebook_renders_mermaid_diagram(
0,
0,
mermaid_block_start,
)),
)
.with_step(move_notebook_cursor_to_offset(0, 0, cursor_offset))
.with_step(
TestStep::new("Backspace from inside rendered Mermaid")
.with_keystrokes(&["backspace"])
.add_assertion(assert_notebook_contents(0, 0, "Before\nAfter")),
)
}
@@ -0,0 +1,466 @@
//! Integration tests for pane restoration functionality.
//! Tests the ability to restore closed panes using cmd+shift+t.
use super::{new_builder, Builder};
use std::{collections::HashMap, time::Duration};
use warp::{
cmd_or_ctrl_shift,
features::FeatureFlag,
integration_testing::{
pane_group::assert_focused_pane_index,
step::new_step_with_default_assertions,
terminal::{
execute_command, util::ExpectedExitStatus, validate_block_output_on_finished_block,
wait_until_bootstrapped_pane, wait_until_bootstrapped_single_pane_for_tab,
},
workspace::{assert_tab_count, trigger_undo_close},
},
};
/// Tests the basic pane restoration workflow:
/// 1. Split off a pane
/// 2. Run a simple command in it
/// 3. Close the pane
/// 4. Restore the pane with cmd+shift+t
/// 5. Assert the pane is restored in the correct location with previous state
pub fn test_restore_single_closed_pane() -> Builder {
FeatureFlag::UndoClosedPanes.set_enabled(true);
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Split off a new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(execute_command(
0,
1,
"echo \"hello world\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Close the pane")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.add_assertion(assert_focused_pane_index(0, 0)),
)
.with_step(trigger_undo_close())
.with_step(
new_step_with_default_assertions("Verify we still have one tab after restore attempt")
.add_assertion(assert_tab_count(1)),
)
.with_step(
new_step_with_default_assertions("Verify pane was restored with correct state")
.set_pause_on_failure(std::time::Duration::from_secs(30))
.add_assertion(assert_focused_pane_index(0, 1))
.add_assertion(move |app, window_id| {
validate_block_output_on_finished_block(
&"hello world",
0, // tab index
1, // pane index - the restored pane should be at index 1
window_id,
app,
)
}),
)
}
/// Tests complex pane restoration workflow with multiple panes:
/// 1. Start with one pane, split twice to create 3 panes total
/// 2. Run unique commands in each pane
/// 3. Close two of the panes
/// 4. Restore both closed panes using undo close twice
/// 5. Assert both panes are restored correctly with their previous state
pub fn test_restore_multiple_closed_panes() -> Builder {
FeatureFlag::UndoClosedPanes.set_enabled(true);
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command(
0,
0,
"echo \"pane0\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Split off first new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(execute_command(
0,
1,
"echo \"pane1\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Split off second new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 2)),
)
.with_step(wait_until_bootstrapped_pane(0, 2))
.with_step(execute_command(
0,
2,
"echo \"pane2\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Close pane 2")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(
new_step_with_default_assertions("Close pane 1")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.add_assertion(assert_focused_pane_index(0, 0)),
)
.with_step(trigger_undo_close().add_assertion(assert_focused_pane_index(0, 1)))
.with_step(
new_step_with_default_assertions("Verify first restored pane has correct state")
.set_pause_on_failure(std::time::Duration::from_secs(30))
.add_assertion(move |app, window_id| {
validate_block_output_on_finished_block(
&"pane1", 0, // tab index
1, // pane index - first restored pane should be at index 1
window_id, app,
)
}),
)
.with_step(trigger_undo_close().add_assertion(assert_focused_pane_index(0, 2)))
.with_step(
new_step_with_default_assertions("Verify second restored pane has correct state")
.set_pause_on_failure(std::time::Duration::from_secs(30))
.add_assertion(move |app, window_id| {
validate_block_output_on_finished_block(
&"pane2", 0, // tab index
2, // pane index - second restored pane should be at index 2
window_id, app,
)
}),
)
.with_step(
new_step_with_default_assertions(
"Verify we still have one tab after all restore operations",
)
.add_assertion(assert_tab_count(1)),
)
.with_step(
new_step_with_default_assertions("Verify original pane (pane 0) still has its state")
.add_assertion(move |app, window_id| {
validate_block_output_on_finished_block(
&"pane0", 0, // tab index
0, // pane index - original pane should still be at index 0
window_id, app,
)
}),
)
}
/// Tests that panes are properly cleaned up after the grace period expires:
/// 1. Split off a pane and run a command in it
/// 2. Close the pane
/// 3. Wait for the grace period to expire (5 seconds in test)
/// 4. Attempt to restore the pane with cmd+shift+t
/// 5. Assert the pane is NOT restored because it was cleaned up
pub fn test_undo_close_grace_period_cleanup() -> Builder {
FeatureFlag::UndoClosedPanes.set_enabled(true);
new_builder()
.with_user_defaults(HashMap::from([(
"UndoCloseGracePeriod".to_owned(),
serde_json::to_string(&Duration::from_secs(5))
.expect("Duration should convert to JSON string"),
)]))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Split off a new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(execute_command(
0,
1,
"echo \"hello world\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Close the pane")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.add_assertion(assert_focused_pane_index(0, 0)),
)
.with_step(
new_step_with_default_assertions("Wait for grace period to expire")
.set_timeout(Duration::from_secs(7)), // Wait 7 seconds for 5 second grace period
)
.with_step(
new_step_with_default_assertions("Check pane count before undo close")
.add_assertion(move |app, window_id| {
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
let initial_pane_count = workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
pane_group_view.read(ctx, |pane_group, _| pane_group.pane_count())
});
warpui::async_assert_eq!(initial_pane_count, 1, "Should have exactly one pane after grace period expires - closed pane should be cleaned up")
}),
)
.with_step(trigger_undo_close()
.add_assertion(assert_focused_pane_index(0, 0)) // Should still be focused on original pane
.add_assertion(move |app, window_id| {
// Assert we still only have one pane (no restoration occurred)
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
let pane_count = pane_group_view.read(ctx, |pane_group, _| pane_group.pane_count());
warpui::async_assert_eq!(pane_count, 1, "Should still have only one pane after attempted restore - no pane should be restored")
})
}),
)
.with_step(
new_step_with_default_assertions("Verify we still have one tab after failed restore attempt")
.add_assertion(assert_tab_count(1)),
)
}
/// Tests that closed panes are cleared when pane rearrangement operations begin:
/// 1. Create 3 panes and run commands in each
/// 2. Close one pane (it gets hidden for undo)
/// 3. Start a pane rearrangement operation (resize divider)
/// 4. Attempt to restore the closed pane with cmd+shift+t
/// 5. Assert the pane is NOT restored because it was cleared during rearrangement
pub fn test_closed_panes_cleared_on_rearrangement() -> Builder {
FeatureFlag::UndoClosedPanes.set_enabled(true);
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command(
0,
0,
"echo \"original_pane\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Split off first new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(execute_command(
0,
1,
"echo \"middle_pane\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Split off second new pane to the right")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 2)),
)
.with_step(wait_until_bootstrapped_pane(0, 2))
.with_step(execute_command(
0,
2,
"echo \"third_pane\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
// Close the middle pane by using a direct operation targeting pane index 1
warp::integration_testing::pane_group::close_pane_by_index(
0, // tab index
1, // pane index - the middle pane
),
)
.with_step(
new_step_with_default_assertions("Verify we have 2 visible panes after closing one")
.add_assertion(move |app, window_id| {
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
let (visible_pane_count, total_pane_count) = pane_group_view.read(ctx, |pane_group, _| {
(pane_group.visible_pane_count(), pane_group.pane_count())
});
if visible_pane_count != 2 {
warpui::integration::AssertionOutcome::failure(format!("Should have 2 visible panes after closing one (got {visible_pane_count} visible, {total_pane_count} total)"))
} else {
warpui::integration::AssertionOutcome::Success
}
})
}),
)
.with_step(
// Trigger pane rearrangement by moving panes
warp::integration_testing::pane_group::move_pane_by_indices(
0,
0,
1,
warp::pane_group::tree::Direction::Right,
),
)
.with_step(
// Trigger undo close - should NOT restore the pane since rearrangement cleared it
trigger_undo_close()
)
.with_step(
new_step_with_default_assertions("Verify pane was NOT restored - still have same visible panes")
.add_assertion(move |app, window_id| {
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
let visible_pane_count = pane_group_view.read(ctx, |pane_group, _| pane_group.visible_pane_count());
// After rearrangement, we should still have the same visible panes (no restoration)
// The exact count might vary based on how the move operation affects the layout
if visible_pane_count < 1 {
warpui::integration::AssertionOutcome::failure(format!("Should have at least 1 visible pane after undo close attempt, got {visible_pane_count}"))
} else {
warpui::integration::AssertionOutcome::Success
}
})
}),
)
.with_step(
trigger_undo_close().add_assertion(move |app, window_id| {
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
let visible_pane_count = pane_group_view.read(ctx, |pane_group, _| pane_group.visible_pane_count());
// Should still have the same panes, no restoration should occur
if visible_pane_count < 1 {
warpui::integration::AssertionOutcome::failure(format!("Should have at least 1 visible pane after second undo close attempt, got {visible_pane_count}"))
} else {
warpui::integration::AssertionOutcome::Success
}
})
})
)
.with_step(
new_step_with_default_assertions("Verify remaining pane has expected state")
.add_assertion(move |app, window_id| {
let workspace_view = warp::integration_testing::view_getters::workspace_view(app, window_id);
let visible_pane_count = workspace_view.read(app, |workspace, ctx| {
let pane_group_view = workspace.get_pane_group_view(0).expect("should have tab 0");
pane_group_view.read(ctx, |pane_group, _| pane_group.visible_pane_count())
});
if visible_pane_count == 1 {
// If we have 1 pane, it should be the third_pane
validate_block_output_on_finished_block(
&"third_pane",
0, // tab index
0, // pane index - only remaining pane
window_id,
app,
)
} else if visible_pane_count == 2 {
// If we have 2 panes, check for both original and third
let original_result = validate_block_output_on_finished_block(
&"original_pane",
0, 1, window_id, app,
);
let third_result = validate_block_output_on_finished_block(
&"third_pane",
0, 0, window_id, app,
);
match (original_result, third_result) {
(warpui::integration::AssertionOutcome::Success, warpui::integration::AssertionOutcome::Success) => {
warpui::integration::AssertionOutcome::Success
}
_ => warpui::integration::AssertionOutcome::failure("Expected to find both original_pane and third_pane content".to_string())
}
} else {
warpui::integration::AssertionOutcome::failure(format!("Unexpected pane count: {visible_pane_count}"))
}
}),
)
}
/// Tests that closing the last visible pane in a tab properly closes the tab:
/// 1. Create a single pane in a tab with a command
/// 2. Create a new tab to verify multiple tabs exist
/// 3. Return to first tab and close its only pane
/// 4. Verify the tab is closed (not just showing empty)
/// 5. Restore the pane and verify it creates a new tab
pub fn test_tab_closes_when_last_visible_pane_closed() -> Builder {
FeatureFlag::UndoClosedPanes.set_enabled(true);
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command(
0,
0,
"echo \"first_tab_content\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Create a new tab")
.with_keystrokes(&[cmd_or_ctrl_shift("t")])
.add_assertion(assert_tab_count(2)),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(execute_command(
1,
0,
"echo \"second_tab_content\"".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
new_step_with_default_assertions("Switch back to first tab")
.with_keystrokes(&["cmdorctrl-1"])
.add_assertion(assert_focused_pane_index(0, 0)),
)
.with_step(
new_step_with_default_assertions("Close the only pane in first tab")
.with_keystrokes(&[cmd_or_ctrl_shift("w")])
.set_timeout(Duration::from_secs(10)) // Allow time for tab closure
.add_assertion(assert_tab_count(1)) // Tab should be closed, leaving only one tab
.add_assertion(assert_focused_pane_index(0, 0)), // Should be focused on the remaining tab (previously tab 1)
)
.with_step(
new_step_with_default_assertions("Verify we're now on the second tab's content")
.add_assertion(move |app, window_id| {
validate_block_output_on_finished_block(
&"second_tab_content",
0, // tab index - this is now the first (and only) tab
0, // pane index
window_id,
app,
)
}),
)
.with_step(
trigger_undo_close()
.set_timeout(Duration::from_secs(15)) // Allow extra time for tab restoration
.add_assertion(assert_tab_count(2)), // Should now have 2 tabs again
)
.with_step(
new_step_with_default_assertions("Wait for restored tab to be ready")
.set_timeout(Duration::from_secs(10)), // Allow time for tab setup
)
.with_step(
new_step_with_default_assertions("Verify restored pane has correct content in new tab")
.set_timeout(Duration::from_secs(20)) // Allow extra time for content validation
.set_pause_on_failure(std::time::Duration::from_secs(30))
.add_assertion(move |app, window_id| {
let workspace_view =
warp::integration_testing::view_getters::workspace_view(app, window_id);
workspace_view.read(app, |workspace, _ctx| {
let focused_tab_idx = workspace.active_tab_index();
// The restored tab should be the currently focused tab (which should have first_tab_content)
validate_block_output_on_finished_block(
&"first_tab_content",
focused_tab_idx, // Use the currently focused tab
0, // pane index
window_id,
app,
)
})
}),
)
}
@@ -0,0 +1,125 @@
use std::fs;
use std::path::PathBuf;
use warpui::integration::{AssertionOutcome, TestStep};
use crate::Builder;
use super::wait_until_bootstrapped_single_pane_for_tab;
/// Returns the current `$HOME` as a [`PathBuf`].
/// In integration tests, `HOME` is overridden to a hermetic temp directory.
fn home_dir() -> PathBuf {
PathBuf::from(std::env::var("HOME").expect("HOME should be set in integration tests"))
}
/// Verifies that `migrate_config_dir_via_symlinks` creates symlinks from
/// an old config directory into a new one, skipping macOS metadata files.
pub fn test_preview_config_dir_migration() -> Builder {
Builder::new()
.with_setup(|utils| {
let home = utils.test_dir();
let old_dir = home.join(".warp");
// Populate the old config directory with representative entries.
fs::create_dir_all(old_dir.join("themes")).expect("create themes dir");
fs::write(old_dir.join("keybindings.yaml"), "bindings")
.expect("write keybindings.yaml");
fs::write(old_dir.join("themes").join("dark.yaml"), "theme").expect("write dark.yaml");
fs::create_dir_all(old_dir.join("workflows")).expect("create workflows dir");
fs::write(old_dir.join(".mcp.json"), "{}").expect("write .mcp.json");
// Files that should be excluded from the migration.
fs::write(old_dir.join(".DS_Store"), "metadata").expect("write .DS_Store");
fs::write(old_dir.join("._somefile"), "resource fork").expect("write ._somefile");
fs::write(old_dir.join("settings.toml"), "[settings]").expect("write settings.toml");
// Run the migration. We call the inner helper directly because the
// integration channel is Integration, not Preview, so the public
// entry point would no-op.
let new_dir = home.join(".warp-preview");
warp::integration_testing::preview_config_migration::run_config_dir_symlink_migration(
&old_dir, &new_dir,
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Assert symlinks were created correctly")
.add_named_assertion(
"new directory exists and is a real directory",
|_app, _window_id| {
let new_dir = home_dir().join(".warp-preview");
if !new_dir.is_dir() {
return AssertionOutcome::failure(
".warp-preview should be a directory".to_string(),
);
}
// It should be a real directory, not a symlink.
let is_symlink = new_dir
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
if is_symlink {
return AssertionOutcome::failure(
".warp-preview should not itself be a symlink".to_string(),
);
}
AssertionOutcome::Success
},
)
.add_named_assertion(
"expected entries are symlinks pointing to old dir",
|_app, _window_id| {
let home = home_dir();
let old_dir = home.join(".warp");
let new_dir = home.join(".warp-preview");
for name in ["keybindings.yaml", "themes", "workflows", ".mcp.json"] {
let link = new_dir.join(name);
let expected_target = old_dir.join(name);
match fs::read_link(&link) {
Ok(target) => {
// Canonicalize both sides before comparing. On some
// CI runners, the hermetic `$HOME` is reached through
// a symlinked mount (e.g. `/Volumes/cache/...` vs
// `/Users/runner/...`), so the raw symlink target and
// the path we compute from `$HOME` may differ even
// when they point to the same file.
let target_canonical = fs::canonicalize(&target)
.unwrap_or_else(|_| target.clone());
let expected_canonical = fs::canonicalize(&expected_target)
.unwrap_or_else(|_| expected_target.clone());
if target_canonical != expected_canonical {
return AssertionOutcome::failure(format!(
"{name}: symlink points to {} (canonical {}), expected {} (canonical {})",
target.display(),
target_canonical.display(),
expected_target.display(),
expected_canonical.display(),
));
}
}
Err(err) => {
return AssertionOutcome::failure(format!(
"{name}: not a symlink: {err}",
));
}
}
}
AssertionOutcome::Success
},
)
.add_named_assertion("excluded files were not symlinked", |_app, _window_id| {
let new_dir = home_dir().join(".warp-preview");
for name in [".DS_Store", "._somefile", "settings.toml"] {
if new_dir.join(name).exists() {
return AssertionOutcome::failure(format!(
"{name} should not be symlinked",
));
}
}
AssertionOutcome::Success
}),
)
}
+88
View File
@@ -0,0 +1,88 @@
use warp::integration_testing::{
rules::{
assert_rule_count, assert_rule_exists, assert_rule_pane_open, create_a_personal_rule,
open_rule_pane, update_rule_content,
},
step::new_step_with_default_assertions,
terminal::wait_until_bootstrapped_single_pane_for_tab,
window::save_active_window_id,
};
use super::{new_builder, Builder};
/// Test creating a rule
pub fn test_rule_creation() -> Builder {
let key = "rule";
let rule_content = "Never use unwrap in Rust.";
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
create_a_personal_rule(key, "Rust", rule_content).add_assertion(assert_rule_count(1)),
)
.with_step(
new_step_with_default_assertions("Verify rule content")
.add_named_assertion_with_data_from_prior_step(
"Check rule exists with correct content",
assert_rule_exists(key, rule_content),
),
)
}
/// Test updating a rule's content
pub fn test_rule_update() -> Builder {
let key = "rule";
let rule_content = "Old rule content";
let new_rule_content = "New rule content";
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
create_a_personal_rule(key, "Test Rule", rule_content)
.add_assertion(assert_rule_count(1)),
)
.with_step(
new_step_with_default_assertions("Verify original content")
.add_named_assertion_with_data_from_prior_step(
"Check original rule content",
assert_rule_exists(key, rule_content),
),
)
.with_step(update_rule_content(key, new_rule_content))
.with_step(
new_step_with_default_assertions("Verify updated content")
.add_named_assertion_with_data_from_prior_step(
"Check updated rule content",
assert_rule_exists(key, new_rule_content),
),
)
}
// Test opening rule pane at the correct rule
pub fn test_rule_pane_opening() -> Builder {
let key = "rule";
let window_id = "main_window";
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
create_a_personal_rule(
key,
"Pane Test Rule",
"This rule is for testing pane opening",
)
.add_assertion(assert_rule_count(1))
.add_assertion(save_active_window_id(window_id)),
)
.with_step(
create_a_personal_rule(
"rule_2",
"Pane Test Rule #2",
"This rule is for testing pane opening #2",
)
.add_assertion(assert_rule_count(2)),
)
.with_step(
open_rule_pane(window_id, key).add_named_assertion_with_data_from_prior_step(
"Check rule pane opens",
assert_rule_pane_open(key),
),
)
}
+294
View File
@@ -0,0 +1,294 @@
use warp::integration_testing::terminal::{
initialize_secret_regexes, open_context_menu_for_selected_block,
};
use warp::{
integration_testing::{
clipboard::assert_clipboard_contains_string,
secret_redaction::{assert_secret_tooltip_open, assert_secrets_redacted_for_ai},
settings::toggle_setting,
step::new_step_with_default_assertions,
terminal::{
assert_selected_block_index_is_last_renderable,
execute_command_for_single_terminal_in_tab, run_alt_grid_program,
util::ExpectedExitStatus, wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::single_terminal_view,
},
settings_view::{PrivacyPageAction, SettingsAction},
terminal::model::{index::Point, terminal_model::WithinModel},
};
use warpui::{async_assert, integration::TestStep};
use crate::util::skip_if_powershell_core_2303;
use super::{new_builder, Builder};
pub fn test_secret_is_obfuscated_on_copy() -> Builder {
let phone_number = "123-456-7890";
let phone_number_obfuscated = "************";
new_builder()
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_step(execute_command_for_single_terminal_in_tab(
0,
format!("echo {phone_number}"),
ExpectedExitStatus::Success,
phone_number,
))
.with_step(
new_step_with_default_assertions("Select block")
.with_keystrokes(&["cmdorctrl-up"])
.add_named_assertion(
"ensure block is selected",
assert_selected_block_index_is_last_renderable(),
),
)
.with_steps(open_context_menu_for_selected_block())
.with_step(
new_step_with_default_assertions("Arrow down and select copy Command")
.with_keystrokes(&["down", "down", "enter"])
.add_assertion(assert_clipboard_contains_string(format!(
"echo {phone_number_obfuscated}"
))),
)
}
pub fn test_secret_tooltip_shows_on_click() -> Builder {
let phone_number = "123-456-7890";
new_builder()
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_step(execute_command_for_single_terminal_in_tab(
0,
format!("echo {phone_number}"),
ExpectedExitStatus::Success,
phone_number,
))
.with_step(
// Note: ideally, we shouldn't hardcode a secret handle ID here but we're doing this
// for now. This is affected by the addition/removal of new `GridType`s!
new_step_with_default_assertions("Click on secret to show tooltip")
.with_click_on_saved_position("terminal_view:first_cell_in_secret_1")
.add_assertion(assert_secret_tooltip_open(true)),
)
}
pub fn test_secret_tooltip_respects_safe_mode_setting() -> Builder {
let phone_number = "123-456-7890";
new_builder()
// TODO(CORE-2732): Flakey on Powershell (Linux)
.set_should_run_test(skip_if_powershell_core_2303)
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
))) // Safe mode is now enabled.
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_step(execute_command_for_single_terminal_in_tab(
0,
format!("echo {phone_number}"),
ExpectedExitStatus::Success,
phone_number,
))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
))) // Safe mode is now disabled.
.with_step(
// Note: ideally, we shouldn't hardcode a secret handle ID here but we're doing this
// for now. This is affected by the addition/removal of new `GridType`s!
new_step_with_default_assertions("Click on secret to show tooltip")
.with_click_on_saved_position("terminal_view:first_cell_in_secret_1")
.add_assertion(assert_secret_tooltip_open(false)),
)
}
pub fn test_copy_secret_respects_safe_mode_setting() -> Builder {
let phone_number = "123-456-7890";
new_builder()
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_step(execute_command_for_single_terminal_in_tab(
0,
format!("echo {phone_number}"),
ExpectedExitStatus::Success,
phone_number,
))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(
new_step_with_default_assertions("Select block")
.with_keystrokes(&["cmdorctrl-up"])
.add_named_assertion(
"ensure block is selected",
assert_selected_block_index_is_last_renderable(),
),
)
.with_steps(open_context_menu_for_selected_block())
.with_step(
new_step_with_default_assertions("Arrow down and select copy Command")
.with_keystrokes(&["down", "down", "enter"])
.add_assertion(assert_clipboard_contains_string(format!(
"echo {phone_number}"
))),
)
}
pub fn test_alt_screen_secret_detection() -> Builder {
let phone_number = "123-456-7890";
let exit_step = TestStep::new("Exit vim")
.with_keystrokes(&["escape"])
.with_typed_characters(&[":q!"])
.with_keystrokes(&["enter"]);
new_builder()
// TODO(CORE-2732): Flakey on Powershell
.set_should_run_test(skip_if_powershell_core_2303)
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
.with_steps(run_alt_grid_program(
"vim",
0,
0,
exit_step,
vec![
TestStep::new("Type in secret").with_typed_characters(&["i", phone_number]),
TestStep::new("Check that secret exists").add_assertion(|app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let secret =
model.secret_at_point(&WithinModel::AltScreen(Point::new(0, 0)));
async_assert!(secret.is_some(), "Secret exists")
})
}),
TestStep::new("Check that secret is obfuscated").add_assertion(|app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let secret =
model.secret_at_point(&WithinModel::AltScreen(Point::new(0, 0)));
let secret = secret
.expect("Secret existence verified by previous step")
.1;
async_assert!(secret.is_obfuscated(), "Secret is obfuscated")
})
}),
],
))
}
pub fn test_secret_case_sensitivity() -> Builder {
// Test the secret redaction respects case by default
new_builder()
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)))
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
)))
// AWS Access ID pattern is case-sensitive by default
.with_step(execute_command_for_single_terminal_in_tab(
0,
"echo 'AKIAABC123456789DEFG akiaabc123456789defg'".to_string(),
ExpectedExitStatus::Success,
"AKIAABC123456789DEFG akiaabc123456789defg",
))
.with_step(
new_step_with_default_assertions("Select block")
.with_keystrokes(&["cmdorctrl-up"])
.add_named_assertion(
"ensure block is selected",
assert_selected_block_index_is_last_renderable(),
),
)
.with_steps(open_context_menu_for_selected_block())
.with_step(
new_step_with_default_assertions("Arrow down and select copy Command")
.with_keystrokes(&["down", "down", "enter"])
// Only the uppercase ID should be redacted since pattern requires uppercase
.add_assertion(assert_clipboard_contains_string(
"echo '******************** akiaabc123456789defg'".to_string(),
)),
)
}
pub fn test_secrets_are_always_redacted_in_ai_inputs() -> Builder {
let phone_number = "123-456-7890";
let secret_api_key = "sk-1234567890abcdef";
let expected_redacted_phone = "************";
let expected_redacted_api_key = "******************";
let test_command = "echo 'Phone: 123-456-7890 API: sk-1234567890abcdef'.";
let test_output = "Phone: 123-456-7890 API: sk-1234567890abcdef.";
new_builder()
.set_should_run_test(skip_if_powershell_core_2303)
.with_step(initialize_secret_regexes())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Test case 1: Strikethrough mode - secrets should be redacted from AI inputs
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
))) // Enable safe mode (strikethrough by default since hide_secrets_in_block_list defaults to false)
.with_step(execute_command_for_single_terminal_in_tab(
0,
test_command.to_string(),
ExpectedExitStatus::Success,
test_output,
))
.with_step(
new_step_with_default_assertions("Test strikethrough mode redaction").add_assertion(
assert_secrets_redacted_for_ai(
test_output.to_string(),
expected_redacted_phone.to_string(),
expected_redacted_api_key.to_string(),
phone_number.to_string(),
secret_api_key.to_string(),
),
),
)
// Test case 2: Full obfuscation mode - secrets should also be redacted
.with_step(toggle_setting(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleHideSecretsInBlockList,
))) // Enable full hiding (Yes mode)
.with_step(
new_step_with_default_assertions("Test full obfuscation mode redaction").add_assertion(
assert_secrets_redacted_for_ai(
test_output.to_string(),
expected_redacted_phone.to_string(),
expected_redacted_api_key.to_string(),
phone_number.to_string(),
secret_api_key.to_string(),
),
),
)
}
@@ -0,0 +1,548 @@
use settings::{RespectUserSyncSetting, SyncToCloud};
use warp::{
features::FeatureFlag,
integration_testing::{
self,
notebook::{
assert_cloud_preference_exists, assert_notebook_contents,
assert_notebook_metadata_revision,
},
step::{new_step_with_default_assertions, new_step_with_default_assertions_for_pane},
tab::assert_pane_title,
terminal::wait_until_bootstrapped_single_pane_for_tab,
view_getters::single_terminal_view_for_tab,
workflow::assert_workflow_metadata_revision,
},
settings::Preference,
settings_view::{SettingsSection, SettingsView},
sqlite_testing::set_user_and_hostname_for_blocks,
terminal::{
model::{session::get_local_hostname, terminal_model::BlockIndex},
shell::ShellType,
History, ShellHost, TerminalView,
},
workspace::Workspace,
};
use warpui::{
async_assert_eq,
integration::{AssertionOutcome, TestStep},
SingletonEntity, ViewHandle,
};
use crate::util::{get_local_user, tab_title_in_home_dir};
use super::{new_builder, Builder, TEST_ONLY_ASSETS};
pub fn test_session_restoration() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
// Three tabs is a snapshot with three tabs that have the cwd None.
"three_tabs.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(wait_until_bootstrapped_single_pane_for_tab(2))
.with_step(
new_step_with_default_assertions("Assert the app state").add_assertion(
move |app, window_id| {
// There should be three tabs.
let workspace_views: Vec<ViewHandle<Workspace>> =
app.views_of_type(window_id).expect("Workspace must exist");
let workspace = workspace_views.first().expect("Workspace must exist");
workspace.read(app, |workspace, _| assert_eq!(workspace.tab_count(), 3));
// There should be three terminal views.
let terminal_views: Vec<ViewHandle<TerminalView>> =
app.views_of_type(window_id).expect("Terminals must exist");
assert_eq!(terminal_views.len(), 3);
// The pwd should be ~ for each one.
for terminal_view in terminal_views {
terminal_view.read(app, |terminal_view, _| {
let model = terminal_view.model.lock();
let pwd = model
.block_list()
.active_block()
.user_friendly_pwd()
.expect("Should have pwd");
assert_eq!(pwd, "~");
});
}
AssertionOutcome::Success
},
),
)
}
/// Saved blocks run on different hosts/shells should NOT get added to History::session_commands
/// during session restoration. However, if we have NULL for the shell/host information, it should
/// always get added. The mock data for this case looks like this:
/// | command | output | shell | user | host |
/// | ------------------ | ------------ | ----- | ---------- | ------------- |
/// | echo $TERM_PROGRAM | WarpTerminal | zsh | local:user | local:host |
/// | pwd | / | bash | local:user | local:host |
/// | uname | Linux | zsh | andy | ubuntu-test |
/// | mkdir secrets | secrets | NULL | NULL | NULL |
/// | echo foobar | foobar | pwsh | local:user | local:host |
pub fn test_restored_blocks_on_different_hosts() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_blocks.sqlite",
&integration_testing::persistence::database_file_path(),
);
let local_user = get_local_user();
let local_hostname = get_local_hostname().expect("Failed to retrieve system hostname.");
set_user_and_hostname_for_blocks(local_user, local_hostname);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert the app state").add_assertion(
move |app, window_id| {
let terminal_views: Vec<ViewHandle<TerminalView>> = app
.views_of_type(window_id)
.expect("Should have views of type TerminalView after bootstrapping");
assert_eq!(terminal_views.len(), 1);
let terminal_view = &terminal_views[0];
terminal_view.read(app, |terminal, ctx| {
History::handle(ctx).read(app, |history, ctx| {
let session = terminal
.active_block_session_id()
.and_then(|session_id| terminal.sessions(ctx).get(session_id))
.expect("terminal should have active session after bootstrap");
let local_user = get_local_user();
let local_hostname =
get_local_hostname().expect("Failed to retrieve system hostname.");
let shell_type = session.shell().shell_type();
let shell_host = ShellHost {
shell_type,
user: local_user,
hostname: local_hostname,
};
let hist_list = &history.session_commands()[&shell_host];
match shell_type {
ShellType::Zsh => {
assert_eq!(hist_list.len(), 2);
assert_eq!(
hist_list[0].command, "echo $TERM_PROGRAM",
"history item 1 for Zsh"
);
async_assert_eq!(
hist_list[1].command,
"mkdir secrets",
"history item 2 for Zsh"
)
}
ShellType::Bash => {
assert_eq!(hist_list.len(), 2);
assert_eq!(
hist_list[0].command, "pwd",
"history items for Bash"
);
async_assert_eq!(
hist_list[1].command,
"mkdir secrets",
"history item 2 for Bash"
)
}
ShellType::Fish => {
assert_eq!(hist_list.len(), 1, "fish has no restored commands");
async_assert_eq!(
hist_list[0].command,
"mkdir secrets",
"history items for fish"
)
}
ShellType::PowerShell => {
assert_eq!(hist_list.len(), 2);
assert_eq!(
hist_list[0].command, "mkdir secrets",
"history items for PowerShell"
);
async_assert_eq!(
hist_list[1].command,
"echo foobar",
"history item 2 for PowerShell"
)
}
}
})
})
},
),
)
}
/// Regression test to ensure we don't ever crash in this scenario.
pub fn test_restore_snapshot_with_deleted_cwd() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"deleted_cwd.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert there's one terminal with ~ as the pwd")
.add_assertion(move |app, window_id| {
// There should be one terminal view.
let terminal_views: Vec<ViewHandle<TerminalView>> =
app.views_of_type(window_id).expect("Terminal must exist");
assert_eq!(terminal_views.len(), 1);
let terminal_view = terminal_views
.first()
.expect("There is exactly one terminal view");
terminal_view.read(app, |terminal_view, _| {
let model = terminal_view.model.lock();
let pwd = model
.block_list()
.active_block()
.user_friendly_pwd()
.expect("Should have pwd");
assert_eq!(pwd, "~");
});
AssertionOutcome::Success
}),
)
}
// Note: this test is brittle b/c it depends on sqlite having accurate paths to
// the bash and zsh executables in the test runner. If we have a mechanism for it,
// it would be nice to be able to modify the sqlite template to incldue the proper
// paths, rather than having to hardcode them in advance.
pub fn test_session_restoration_with_multiple_shells() -> Builder {
FeatureFlag::ShellSelector.set_enabled(true);
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test_restoring_tabs_with_different_shells.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(
new_step_with_default_assertions("Assert that tabs are of different shells")
.add_assertion(move |app, window_id| {
let terminal_views: Vec<ViewHandle<TerminalView>> =
app.views_of_type(window_id).expect("Terminals must exist");
assert_eq!(terminal_views.len(), 2);
let bash_view = &terminal_views[0];
let zsh_view = &terminal_views[1];
assert_eq!(
zsh_view.read(app, |session, ctx| session.active_session_shell_type(ctx)),
Some(ShellType::Zsh)
);
assert_eq!(
bash_view.read(app, |session, ctx| session.active_session_shell_type(ctx)),
Some(ShellType::Bash)
);
AssertionOutcome::Success
}),
)
}
/// Background output should be restored inline with regular command blocks.
/// The session being restored is:
/// ```shell
/// $ (sleep 5l echo "background output") &
/// [1] 1512
/// $ echo foreground 1
/// foreground 1
/// background output
/// [1] + done ( sleep 5; echo "background output"; )
/// $ echo foreground 2
/// foreground 2
/// ```
pub fn test_restore_snapshot_with_background_output() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_background_blocks.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert the background output is restored")
.add_named_assertion("block list contents", move |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |terminal, _| {
let model = terminal.model.lock();
let blocks = model.block_list();
let command_block = blocks
.block_at(BlockIndex::from(0))
.expect("command block should exist");
assert!(!command_block.is_background());
assert_eq!(
&command_block.command_to_string(),
r#"(sleep 5; echo "background output") &"#
);
assert_eq!(&command_block.output_to_string(), "[1] 1512");
let foreground_block_1 = blocks
.block_at(BlockIndex::from(1))
.expect("block should exist");
assert!(!foreground_block_1.is_background());
assert_eq!(foreground_block_1.command_to_string(), "echo foreground 1");
let background_block = blocks
.block_at(BlockIndex::from(2))
.expect("block should exist");
assert!(background_block.is_background());
assert!(background_block.command_to_string().is_empty());
assert_eq!(
background_block.output_to_string(),
r#"background output
[1] + done ( sleep 5; echo "background output"; )"#
);
let foreground_block_2 = blocks
.block_at(BlockIndex::from(3))
.expect("block should exist");
assert!(!foreground_block_2.is_background());
assert_eq!(foreground_block_2.command_to_string(), "echo foreground 2");
AssertionOutcome::Success
})
}),
)
}
/// Tests restoring a snapshot that includes notebook panes.
///
/// The snapshot has a single window with one tab, containing:
/// * A notebook pane, where the notebook exists
/// * A notebook pane, where the notebook no longer exists
/// * A terminal pane
pub fn test_restore_snapshot_with_notebooks() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_notebooks.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(
TestStep::new("Verify that the notebook panes were restored")
.add_assertion(assert_pane_title(0, 0, "First Notebook"))
// The missing notebook should be replaced with an empty new notebook.
.add_assertion(assert_pane_title(0, 1, "Untitled")),
)
.with_step(
new_step_with_default_assertions_for_pane("Wait for terminal pane to bootstrap", 0, 2)
.add_assertion(assert_pane_title(
0,
2,
tab_title_in_home_dir("test_restore_snapshot_with_notebooks"),
)),
)
.with_step(
TestStep::new("Verify notebook contents")
.add_assertion(assert_notebook_contents(0, 0, "Notebook 1 content"))
.add_assertion(assert_notebook_contents(0, 1, "")),
)
}
/// Test restoring a snapshot that includes workflow panes - the second pane exists, but the first
/// is for a deleted workflow.
pub fn test_restore_snapshot_with_workflows() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_workflows.sqlite",
&integration_testing::persistence::database_file_path(),
)
})
.with_step(
TestStep::new("Verify that the workflow panes were restored")
.add_assertion(assert_pane_title(0, 1, "My Workflow"))
.add_assertion(assert_pane_title(0, 0, "Untitled")),
)
}
/// Tests restoring a snapshot that includes a test json object.
///
/// The test json object has as its contents the string "egpmggresq"
pub fn test_restore_snapshot_with_test_json_object() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test_json_object.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(
TestStep::new("Verify json object contents").add_assertion(
assert_cloud_preference_exists(
Preference::new(
"HonorPS1".to_string(),
"false",
SyncToCloud::Globally(RespectUserSyncSetting::Yes),
)
.expect("error creating preference"),
),
),
)
}
/// Tests restoring a snapshot that has multiple objects with the same shareable_object_id
/// in the metadata table. This test verifies a regression introduced in
/// https://github.com/warpdotdev/warp-internal/pull/7406 and fixed in
/// https://github.com/warpdotdev/warp-internal/pull/7480
///
/// The two objects have server ids Workflow-ftv7on4HwTeixO2xF5hmKf and Notebook-Flbu686H9XDCHZlYRriVpB
/// and shareable_object_id 2.
pub fn test_restore_snapshot_with_common_shareable_metadata_ids() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test_duplicate_shareable_ids.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(TestStep::new("Verify revision of workflow").add_assertion(
assert_workflow_metadata_revision("ftv7on4HwTeixO2xF5hmKf", 1676321629559090),
))
.with_step(TestStep::new("Verify revision of notebook").add_assertion(
assert_notebook_metadata_revision("Flbu686H9XDCHZlYRriVpB", 1690991057168223),
))
}
/// Tests restoring a snapshot that includes a Markdown file pane.
///
/// The snapshot has a single window with one tab, containing:
/// * A terminal pane
/// * A Markdown file pane, `test.md` (backed by [`../../tests/data/test.md`]).
///
/// Normally, we store absolute paths in SQLite for restoring Markdown panes. The test uses a
/// relative path for portability, and assumes it's run from the root of the `integration` crate.
pub fn test_restore_snapshot_with_markdown_file() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"file_notebook.sqlite",
&integration_testing::persistence::database_file_path(),
);
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test.md",
&utils.test_dir().join("docs/test.md"),
);
})
// Wait for the terminal pane to bootstrap first - we need an active session to resolve the
// home directory and context for the notebook pane.
.with_step(
new_step_with_default_assertions_for_pane("Wait for terminal pane to bootstrap", 0, 0)
.add_assertion(assert_pane_title(
0,
0,
tab_title_in_home_dir("test_restore_snapshot_with_markdown_file"),
)),
)
.with_step(
// The pane title isn't set until after the Markdown file is read in, so this verifies
// that both pieces were successful.
TestStep::new("Verify that the notebook pane was restored")
.add_assertion(assert_pane_title(0, 1, "test.md")),
)
}
/// Tests restoring a snapshot that includes a code pane.
///
/// The snapshot has a single window with one tab, containing:
/// * A terminal pane
/// * A code pane, `test.rs` (backed by [`../../tests/data/test.rs`]).
///
/// Normally, we store absolute paths in SQLite for restoring code panes. The test uses a
/// relative path for portability, and assumes it's run from the root of the `integration` crate.
pub fn test_restore_snapshot_with_code_file() -> Builder {
new_builder()
.with_setup(|utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_code.sqlite",
&integration_testing::persistence::database_file_path(),
);
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"test.rs",
&utils.test_dir().join("docs/test.rs"),
);
})
// Wait for the terminal pane to bootstrap first - we need an active session to resolve the
// home directory and context for the notebook pane.
.with_step(
new_step_with_default_assertions_for_pane("Wait for terminal pane to bootstrap", 0, 0)
.add_assertion(assert_pane_title(
0,
0,
tab_title_in_home_dir("test_restore_snapshot_with_code_file"),
)),
)
.with_step(
// The pane title isn't set until after the file is read in, so this verifies
// that both pieces were successful.
TestStep::new("Verify that the code pane was restored")
.add_assertion(assert_pane_title(0, 1, "./docs/test.rs")),
)
}
/// Tests restoring a snapshot that includes a settings pane.
///
/// The snapshot has a single window with one tab, containing:
/// * A terminal pane
/// * A settings pane (with page set to "Referrals")
pub fn test_restore_snapshot_with_settings_page() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"restored_settings.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Verify settings pane restoration")
.add_assertion(assert_pane_title(0, 1, "Settings"))
.add_assertion(move |app, window_id| {
// Verify the settings view exists and is on the Referrals page.
let settings_views: Vec<ViewHandle<SettingsView>> = 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::Referrals
)
})
}),
)
}
@@ -0,0 +1,238 @@
//! Integration tests for the settings file error banner.
//!
//! These tests verify that the workspace shows a warning banner when
//! `settings.toml` contains errors — either the entire file is unparsable
//! or individual setting values are invalid — and that the banner clears
//! when the file is fixed.
use std::time::Duration;
use warp::{
features::FeatureFlag,
integration_testing::{
step::new_step_with_default_assertions,
terminal::wait_until_bootstrapped_single_pane_for_tab, view_getters::workspace_view,
},
};
use warpui::{async_assert, integration::TestStep};
use super::{new_builder, Builder};
/// Helper: returns the path to the TOML settings file.
fn toml_file_path() -> std::path::PathBuf {
warp::settings::user_preferences_toml_file_path()
}
// ---------------------------------------------------------------------------
// Startup: whole file unparsable
// ---------------------------------------------------------------------------
/// Verifies that when `settings.toml` contains invalid TOML syntax on
/// startup, the workspace shows the settings error banner.
pub fn test_settings_error_banner_on_startup_with_invalid_toml() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_setup(move |_utils| {
// Write syntactically invalid TOML before the app starts.
let path = toml_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("should create config dir");
}
std::fs::write(&path, "this is [not valid toml =").expect("should write invalid TOML");
})
.with_step(
wait_until_bootstrapped_single_pane_for_tab(0).add_named_assertion(
"Settings error banner should be visible on startup",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
view.has_settings_file_error_banner(),
"Workspace should show settings error banner for unparsable TOML"
)
})
},
),
)
}
// ---------------------------------------------------------------------------
// Startup: individual invalid value
// ---------------------------------------------------------------------------
/// Verifies that when `settings.toml` contains a syntactically valid TOML
/// file but with an invalid value for a known setting, the workspace shows
/// the settings error banner.
pub fn test_settings_error_banner_on_startup_with_invalid_value() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_setup(move |_utils| {
// Write valid TOML with an invalid value for a bool setting.
// `font_size` expects a float; "not_a_number" will fail deserialization.
let path = toml_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("should create config dir");
}
std::fs::write(&path, "[appearance.text]\nfont_size = \"not_a_number\"\n")
.expect("should write TOML with invalid value");
})
.with_step(
wait_until_bootstrapped_single_pane_for_tab(0).add_named_assertion(
"Settings error banner should be visible for invalid value",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
view.has_settings_file_error_banner(),
"Workspace should show settings error banner for invalid setting value"
)
})
},
),
)
}
// ---------------------------------------------------------------------------
// Reload: whole file becomes unparsable
// ---------------------------------------------------------------------------
/// Verifies that when `settings.toml` becomes unparsable after a file
/// change, the settings error banner appears; and when the file is fixed,
/// the banner disappears.
pub fn test_settings_error_banner_on_reload_with_invalid_toml() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_setup(move |utils| {
// Use a short watcher delay so the reload fires quickly.
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some("10".to_string()));
// Create a valid settings file so the watcher is already tracking
// it. The reload tests modify this file rather than creating a new
// one, which is more reliable for filesystem watchers.
let path = toml_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("should create config dir");
}
std::fs::write(&path, "# valid empty settings\n")
.expect("should write initial valid TOML");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Step 1: No banner initially.
.with_step(
new_step_with_default_assertions("No settings error banner initially")
.add_named_assertion("Banner should not be visible", |app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
!view.has_settings_file_error_banner(),
"Should not show settings error banner with no errors"
)
})
}),
)
// Step 2: Overwrite with invalid TOML to trigger the error banner.
.with_step(
TestStep::new("Write invalid TOML to settings file")
.set_timeout(Duration::from_secs(30))
.with_setup(|_utils| {
let path = toml_file_path();
std::fs::write(&path, "broken [toml =").expect("should write invalid TOML");
})
.add_named_assertion(
"Banner should appear after reload with invalid TOML",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
view.has_settings_file_error_banner(),
"Workspace should show settings error banner after reload"
)
})
},
),
)
// Step 3: Fix the file — banner should disappear.
.with_step(
TestStep::new("Fix the settings file")
.set_timeout(Duration::from_secs(30))
.with_setup(|_utils| {
let path = toml_file_path();
std::fs::write(&path, "# valid empty TOML\n").expect("should write valid TOML");
})
.add_named_assertion(
"Banner should disappear after file is fixed",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
!view.has_settings_file_error_banner(),
"Banner should clear when settings file is fixed"
)
})
},
),
)
}
// ---------------------------------------------------------------------------
// Reload: individual value becomes invalid
// ---------------------------------------------------------------------------
/// Verifies that when an individual setting value becomes invalid after a
/// file change, the settings error banner appears.
pub fn test_settings_error_banner_on_reload_with_invalid_value() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_setup(move |utils| {
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some("10".to_string()));
// Create the settings file at startup so the watcher tracks it.
let path = toml_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("should create config dir");
}
std::fs::write(&path, "[appearance.text]\nfont_size = 14.0\n")
.expect("should write initial valid TOML");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Step 1: Verify no banner with valid settings.
.with_step(
new_step_with_default_assertions("No banner with valid settings").add_named_assertion(
"No banner with valid settings",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
!view.has_settings_file_error_banner(),
"Should not show error banner with valid settings"
)
})
},
),
)
// Step 2: Change the value to something invalid.
.with_step(
TestStep::new("Write invalid setting value")
.set_timeout(Duration::from_secs(30))
.with_setup(|_utils| {
let path = toml_file_path();
std::fs::write(&path, "[appearance.text]\nfont_size = \"not_a_number\"\n")
.expect("should write invalid value");
})
.add_named_assertion(
"Banner should appear for invalid value",
|app, window_id| {
let workspace = workspace_view(app, window_id);
workspace.read(app, |view, _| {
async_assert!(
view.has_settings_file_error_banner(),
"Workspace should show error banner for invalid setting value"
)
})
},
),
)
}
@@ -0,0 +1,106 @@
//! Integration test for the settings file hot-reload pipeline.
//!
//! Verifies that changes to `settings.toml` on disk are picked up by the
//! filesystem watcher and pushed into the in-memory setting models, on every
//! platform where Warp watches `config_local_dir()`.
use settings::Setting as _;
use std::time::Duration;
use warp::{
features::FeatureFlag,
integration_testing::{
step::new_step_with_default_assertions,
terminal::wait_until_bootstrapped_single_pane_for_tab,
},
settings::FontSettings,
};
use warpui::{async_assert_eq, integration::TestStep, SingletonEntity};
use super::{new_builder, Builder};
/// Helper: returns the path to the TOML settings file.
fn toml_file_path() -> std::path::PathBuf {
warp::settings::user_preferences_toml_file_path()
}
/// Verifies the full settings hot-reload pipeline end-to-end: the filesystem
/// watcher detects a change to `settings.toml`, `reload_from_disk` runs, and
/// `reload_all_public_settings` pushes the new value into the in-memory
/// setting model.
pub fn test_settings_file_hot_reload_applies_new_values() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_setup(move |utils| {
// Use a short watcher delay so each reload fires quickly.
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some("10".to_string()));
// Write an initial valid settings file so the watcher is already
// tracking it and the app reads a known value at startup.
let path = toml_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("should create config dir");
}
std::fs::write(&path, "[appearance.text]\nfont_size = 14.0\n")
.expect("should write initial valid TOML");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Step 1: Confirm the initial value was loaded on startup. This
// baseline rules out a later false positive where the "reload" just
// returns the default value.
.with_step(
new_step_with_default_assertions("Initial font_size loaded from disk")
.add_named_assertion("monospace_font_size == 14.0", |app, _| {
app.read(|ctx| {
let val = FontSettings::as_ref(ctx).monospace_font_size.value();
async_assert_eq!(
*val,
14.0,
"startup load should have set font size to 14.0"
)
})
}),
)
// Step 2: Rewrite the file with a different valid value and wait for
// the watcher to push the new value into the in-memory model.
.with_step(
TestStep::new("Hot reload font_size to 18.0")
.set_timeout(Duration::from_secs(30))
.with_setup(|_utils| {
let path = toml_file_path();
std::fs::write(&path, "[appearance.text]\nfont_size = 18.0\n")
.expect("should write updated font size");
})
.add_named_assertion("monospace_font_size == 18.0", |app, _| {
app.read(|ctx| {
let val = FontSettings::as_ref(ctx).monospace_font_size.value();
async_assert_eq!(
*val,
18.0,
"hot reload should have updated font size to 18.0"
)
})
}),
)
// Step 3: Rewrite a second time to confirm the reload is repeatable
// and not a one-shot effect tied to the initial load.
.with_step(
TestStep::new("Hot reload font_size to 16.0")
.set_timeout(Duration::from_secs(30))
.with_setup(|_utils| {
let path = toml_file_path();
std::fs::write(&path, "[appearance.text]\nfont_size = 16.0\n")
.expect("should write second updated font size");
})
.add_named_assertion("monospace_font_size == 16.0", |app, _| {
app.read(|ctx| {
let val = FontSettings::as_ref(ctx).monospace_font_size.value();
async_assert_eq!(
*val,
16.0,
"second hot reload should have updated font size to 16.0"
)
})
}),
)
}
@@ -0,0 +1,86 @@
//! Integration tests for the one-time migration of public settings from the
//! platform-native store into the TOML settings file.
use std::collections::HashMap;
use settings::Setting as _;
use warp::{
features::FeatureFlag,
integration_testing::terminal::wait_until_bootstrapped_single_pane_for_tab,
settings::{BlockVisibilitySettings, ScrollSettings},
};
use warpui::{async_assert, async_assert_eq, integration::AssertionOutcome, SingletonEntity};
use super::{new_builder, Builder};
/// Verifies that when the `SettingsFile` feature flag is enabled and no TOML
/// file exists yet, public settings are migrated from the platform-native
/// store (a JSON file in integration tests) into the TOML settings file.
pub fn test_settings_file_migration_from_native_store() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
let custom_scroll_multiplier: f32 = 7.0;
new_builder()
.with_user_defaults(HashMap::from([
(
"MouseScrollMultiplier".to_owned(),
serde_json::to_string(&custom_scroll_multiplier)
.expect("scroll multiplier should serialize to JSON string"),
),
(
"ShouldShowBootstrapBlock".to_owned(),
serde_json::to_string(&true)
.expect("bool should serialize to JSON string"),
),
]))
.with_step(
wait_until_bootstrapped_single_pane_for_tab(0)
.add_named_assertion(
"Scroll multiplier should have been migrated from native store",
move |app, _window_id| {
let scroll = app.read(|ctx| {
*ScrollSettings::as_ref(ctx).mouse_scroll_multiplier.value()
});
async_assert!(
(scroll - custom_scroll_multiplier).abs() < f32::EPSILON,
"Expected scroll multiplier to be {custom_scroll_multiplier} but got {scroll}"
)
},
)
.add_named_assertion(
"ShouldShowBootstrapBlock should have been migrated from native store",
move |app, _window_id| {
let show = app.read(|ctx| {
*BlockVisibilitySettings::as_ref(ctx)
.should_show_bootstrap_block
.value()
});
async_assert_eq!(
show,
true,
"Expected should_show_bootstrap_block to be true but got {show}"
)
},
)
.add_named_assertion(
"TOML settings file should contain the migrated settings",
move |_app, _window_id| {
let toml_path = warp::settings::user_preferences_toml_file_path();
let contents = match std::fs::read_to_string(&toml_path) {
Ok(c) => c,
Err(err) => {
return AssertionOutcome::failure(format!(
"Failed to read TOML file at {toml_path:?}: {err}"
));
}
};
async_assert!(
contents.contains("mouse_scroll_multiplier")
&& contents.contains("should_show_bootstrap_block"),
"TOML file should contain migrated settings but got:\n{contents}"
)
},
),
)
}
@@ -0,0 +1,228 @@
//! Integration tests for the private/public settings split.
//!
//! These tests verify that public settings are persisted to the TOML file
//! while private settings remain in the platform-native (JSON) store.
use std::collections::HashMap;
use settings::Setting as _;
use warp::{
features::FeatureFlag,
integration_testing::{
step::new_step_with_default_assertions,
terminal::wait_until_bootstrapped_single_pane_for_tab,
},
settings::{CodeSettings, DebugSettings, FontSettings},
};
use warpui::{async_assert, async_assert_eq, integration::TestStep, SingletonEntity};
use super::{new_builder, Builder};
/// Helper: read the TOML settings file from disk and return its contents.
/// Returns an empty string if the file does not exist.
fn read_toml_file() -> String {
let path = warp::settings::user_preferences_toml_file_path();
std::fs::read_to_string(path).unwrap_or_default()
}
/// Helper: read the JSON user preferences file from disk and return its contents.
/// Returns an empty string if the file does not exist.
fn read_json_prefs_file() -> String {
let path = warp::settings::user_preferences_file_path();
std::fs::read_to_string(path).unwrap_or_default()
}
// ---------------------------------------------------------------------------
// test_private_public_settings_routing_with_flag_enabled
// ---------------------------------------------------------------------------
pub fn test_private_public_settings_routing_with_flag_enabled() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Step 1: Set a public setting (FontSize) and a private setting
// (IsShellDebugModeEnabled) to non-default values.
.with_step(
TestStep::new("Set public and private settings").with_action(|app, _, _| {
FontSettings::handle(app).update(app, |settings, ctx| {
settings
.monospace_font_size
.set_value(18.0, ctx)
.expect("should set font size");
});
DebugSettings::handle(app).update(app, |settings, ctx| {
settings
.is_shell_debug_mode_enabled
.set_value(true, ctx)
.expect("should set debug mode");
});
}),
)
// Step 2: Verify TOML file contains the public setting but not the
// private one.
.with_step(
new_step_with_default_assertions("Verify TOML has public, not private (round 1)")
.add_named_assertion("FontSize in TOML", |_, _| {
let toml = read_toml_file();
async_assert!(
toml.contains("font_size"),
"TOML file should contain the updated font size setting"
)
})
.add_named_assertion("IsShellDebugModeEnabled not in TOML", |_, _| {
let toml = read_toml_file();
async_assert!(
!toml.contains("IsShellDebugModeEnabled")
&& !toml.contains("is_shell_debug_mode_enabled"),
"TOML file should not contain the private setting"
)
}),
)
// Step 3: Verify JSON prefs contain the private setting.
.with_step(
new_step_with_default_assertions("Verify JSON has private setting (round 1)")
.add_named_assertion("IsShellDebugModeEnabled in JSON", |_, _| {
let json = read_json_prefs_file();
async_assert!(
json.contains("IsShellDebugModeEnabled"),
"JSON prefs should contain the private setting"
)
}),
)
// Step 4: Set a second pair — public CodeAsDefaultEditor, private
// DismissedCodeToolbeltNewFeaturePopup.
.with_step(
TestStep::new("Set second pair of settings").with_action(|app, _, _| {
CodeSettings::handle(app).update(app, |settings, ctx| {
settings
.code_as_default_editor
.set_value(true, ctx)
.expect("should set code editor");
settings
.dismissed_code_toolbelt_new_feature_popup
.set_value(true, ctx)
.expect("should set dismissed popup");
});
}),
)
// Step 5: Verify second public setting is in TOML, second private is
// in JSON.
.with_step(
new_step_with_default_assertions("Verify second pair routing")
.add_named_assertion("CodeAsDefaultEditor in TOML", |_, _| {
let toml = read_toml_file();
async_assert!(
toml.contains("use_warp_as_default_editor"),
"TOML should contain CodeAsDefaultEditor"
)
})
.add_named_assertion(
"DismissedCodeToolbeltNewFeaturePopup not in TOML",
|_, _| {
let toml = read_toml_file();
async_assert!(
!toml.contains("DismissedCodeToolbeltNewFeaturePopup")
&& !toml.contains("dismissed_code_toolbelt_new_feature_popup"),
"TOML should not contain the private popup setting"
)
},
)
.add_named_assertion("DismissedCodeToolbeltNewFeaturePopup in JSON", |_, _| {
let json = read_json_prefs_file();
async_assert!(
json.contains("DismissedCodeToolbeltNewFeaturePopup"),
"JSON prefs should contain the private popup setting"
)
}),
)
}
// ---------------------------------------------------------------------------
// test_private_settings_preloaded_and_not_leaked_to_toml
// ---------------------------------------------------------------------------
pub fn test_private_settings_preloaded_and_not_leaked_to_toml() -> Builder {
FeatureFlag::SettingsFile.set_enabled(true);
// Pre-populate private settings in the JSON prefs file (the private
// backend for integration tests).
let user_defaults = HashMap::from([
("IsShellDebugModeEnabled".to_owned(), "true".to_owned()),
(
"DismissedCodeToolbeltNewFeaturePopup".to_owned(),
"true".to_owned(),
),
]);
new_builder()
.with_user_defaults(user_defaults)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
// Step 1: Verify the app loaded the pre-populated private settings.
.with_step(
new_step_with_default_assertions("Verify preloaded private settings")
.add_named_assertion("IsShellDebugModeEnabled is true", |app, _| {
app.read(|ctx| {
let val = DebugSettings::as_ref(ctx)
.is_shell_debug_mode_enabled
.value();
async_assert_eq!(*val, true, "preloaded debug mode should be true")
})
})
.add_named_assertion("DismissedCodeToolbeltNewFeaturePopup is true", |app, _| {
app.read(|ctx| {
let val = CodeSettings::as_ref(ctx)
.dismissed_code_toolbelt_new_feature_popup
.value();
async_assert_eq!(*val, true, "preloaded popup dismissed should be true")
})
}),
)
// Step 2: Write a public setting so the TOML file has content.
.with_step(
TestStep::new("Set a public setting to generate TOML content").with_action(
|app, _, _| {
FontSettings::handle(app).update(app, |settings, ctx| {
settings
.monospace_font_size
.set_value(18.0, ctx)
.expect("should set font size");
});
},
),
)
// Step 3: Verify TOML has the public setting but not the private ones.
.with_step(
new_step_with_default_assertions("TOML has public, not private")
.add_named_assertion("FontSize in TOML", |_, _| {
let toml = read_toml_file();
async_assert!(
toml.contains("font_size"),
"TOML should contain the public font size setting"
)
})
.add_named_assertion("No private keys in TOML", |_, _| {
let toml = read_toml_file();
async_assert!(
!toml.contains("IsShellDebugModeEnabled")
&& !toml.contains("is_shell_debug_mode_enabled")
&& !toml.contains("DismissedCodeToolbeltNewFeaturePopup")
&& !toml.contains("dismissed_code_toolbelt_new_feature_popup"),
"TOML should not contain any private setting keys"
)
}),
)
// Step 4: Verify JSON prefs still have both private settings.
.with_step(
new_step_with_default_assertions("JSON has both private settings").add_named_assertion(
"Private settings in JSON",
|_, _| {
let json = read_json_prefs_file();
async_assert!(
json.contains("IsShellDebugModeEnabled")
&& json.contains("DismissedCodeToolbeltNewFeaturePopup"),
"JSON prefs should contain both private settings"
)
},
),
)
}
+328
View File
@@ -0,0 +1,328 @@
use std::collections::HashMap;
use crate::Builder;
use regex::Regex;
use settings::Setting as _;
use warp::{
features::FeatureFlag,
integration_testing::{
step::new_step_with_default_assertions,
subshell::{
accept_tmux_install, assert_subshell_banner_is_showing,
assert_subshell_is_bootstrapped, enter_ssh_command, enter_ssh_password,
run_exit_command, setup_gcloud_sdk, trigger_subshell_bootstrap,
wait_for_password_prompt,
},
terminal::{
assert_active_block_output_for_single_terminal_in_tab,
assert_long_running_block_executing_for_single_terminal_in_tab,
execute_command_for_single_terminal_in_tab,
util::{current_shell_starter_and_version, nonce, ExactLine, ExpectedExitStatus},
validate_block_output, wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::{single_terminal_view, single_terminal_view_for_tab},
},
terminal::{
model::bootstrap::BootstrapStage,
session_settings::{StartupShell, StartupShellOverride},
shell::ShellType,
},
};
use warpui::{
async_assert, async_assert_eq,
integration::{AssertionCallback, AssertionOutcome, TestStep},
};
use super::new_builder;
/// Verifies that the active block is part of a remote session.
fn assert_active_block_is_remote(user: &'static str, host: &'static str) -> AssertionCallback {
Box::new(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 active_block = model.block_list().active_block();
let Some(session_id) = active_block.session_id() else {
return AssertionOutcome::PreconditionFailed(
"Active block returned None from shell_host()".into(),
);
};
let Some(session) = view.sessions(ctx).get(session_id) else {
return AssertionOutcome::PreconditionFailed(
"Active block should be part of a known session".into(),
);
};
match async_assert!(
!session.is_local(),
"Active block should be part of a remote session"
) {
AssertionOutcome::Success => {}
failure => return failure,
};
let Some(shell_host) = active_block.shell_host() else {
return AssertionOutcome::PreconditionFailed(
"Active block returned None from shell_host()".into(),
);
};
match async_assert_eq!(
shell_host.user,
user,
"Remote session did not have the expected user"
) {
AssertionOutcome::Success => {}
failure => return failure,
};
async_assert_eq!(
shell_host.hostname,
host,
"Remote session did not have the expected host"
)
})
})
}
/// Assertion that the MotD message is shown. How we expect it to be shown
/// depends on whether or not the remote shell can be bootstrapped.
fn assert_motd_shown(bootstrapped: bool) -> AssertionCallback {
let motd_regex = Regex::new("Welcome to Ubuntu").expect("Regex should compile");
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let motd_output = if bootstrapped {
let motd_block = model
.block_list()
.blocks()
.iter()
.rev()
.find(|block| block.bootstrap_stage() == BootstrapStage::ScriptExecution)
.expect("MotD block should exist");
// Because of how we move blocks through their stages, output from
// RC files and the MotD goes into the command grid.
motd_block.command_to_string()
} else {
// For non-bootstrapped blocks, the MotD is part of the SSH
// session output in the active block.
model.block_list().active_block().output_to_string()
};
async_assert!(
motd_regex.is_match(&motd_output),
"Expected output to match {motd_regex:?}, but was:\n{motd_output:?}"
)
})
})
}
/// Verifies that the current session is using a login shell.
fn verify_login_shell(shell: &str) -> TestStep {
let command = match shell {
"zsh" => "[[ -o login ]]",
"fish" => "status --is-login",
// For other shells, we don't actually start a login shell but do source /etc/profile.
_ => "test \"$WARP_PROFILE_LOADED\" = true",
};
match shell {
"bash" | "zsh" => execute_command_for_single_terminal_in_tab(
0,
command.into(),
ExpectedExitStatus::Success,
(),
)
.add_assertion(assert_motd_shown(true /* bootstrapped */)),
_ => {
// For non-bootstrapped shells, run the command directly and verify
// the exit status.
let nonce = nonce();
let expected_output = ExactLine::from(format!("{nonce}: 0"));
TestStep::new("Verify login shell")
.with_typed_characters(&[&format!("{command}; echo \"{nonce}\": $?")])
.with_keystrokes(&["enter"])
.add_assertion(assert_active_block_output_for_single_terminal_in_tab(
expected_output,
0,
))
.add_assertion(assert_motd_shown(false /* bootstrapped */))
}
}
}
/// A macro to generate a test function to validate that we are able to
/// bootstrap a given remote shell when using ssh.
macro_rules! generate_can_bootstrap_legacy_ssh_test_for_shell {
($fn_name:ident, $shell:literal) => {
/// Ensure we can successfully ssh into a $shell remote shell and bootstrap it
/// successfully.
pub fn $fn_name() -> Builder {
new_builder()
// TODO(CORE-2333) PowerShell has no SSH wrapper.
.set_should_run_test(|| {
if FeatureFlag::SSHTmuxWrapper.is_enabled() {
return false;
}
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(setup_gcloud_sdk())
.with_step(enter_ssh_command($shell))
.with_step(wait_for_password_prompt(0 /*tab_idx*/, $shell))
.with_step(
enter_ssh_password().set_post_step_pause(std::time::Duration::from_millis(250)),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions(
"Assert active block is part of a remote session",
)
.add_assertion(assert_active_block_is_remote($shell, "ubuntu-14-04")),
)
.with_step(verify_login_shell($shell))
}
};
}
/// A macro to generate a test function to validate that we are able to
/// bootstrap a given remote shell when using ssh.
macro_rules! generate_can_bootstrap_tmux_ssh_test_for_shell {
($fn_name:ident, $shell:literal, $install_tmux:literal) => {
/// Ensure we can successfully ssh into a $shell remote shell and bootstrap it
/// successfully.
pub fn $fn_name() -> Builder {
fn warpify(builder: Builder) -> Builder {
builder
.with_step(enter_ssh_command($shell))
.with_step(wait_for_password_prompt(0 /*tab_idx*/, $shell))
.with_step(
enter_ssh_password()
.set_post_step_pause(std::time::Duration::from_millis(250)),
)
.with_step(assert_subshell_banner_is_showing())
.with_step(trigger_subshell_bootstrap())
}
fn assert_warpification(builder: Builder) -> Builder {
builder
.with_step(assert_subshell_is_bootstrapped(0, 0))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions(
"Assert active block is part of a remote session",
)
.add_assertion(assert_active_block_is_remote($shell, "ubuntu-14-04")),
)
.with_step(verify_login_shell($shell))
}
let builder = new_builder()
// TODO(CORE-2333) PowerShell has no SSH wrapper.
.set_should_run_test(|| {
if !FeatureFlag::SSHTmuxWrapper.is_enabled() {
return false;
}
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(setup_gcloud_sdk());
// Install Tmux
let builder = warpify(builder).with_step(
accept_tmux_install().set_post_step_pause(std::time::Duration::from_secs(3)),
);
// Quit SSH Session once we validate warpificaiton works with Tmux Install
let builder = assert_warpification(builder).with_step(run_exit_command());
// Validate we can Warpify when Tmux is already installed
assert_warpification(warpify(builder))
}
};
}
/// A macro to generate a test function to validate that we are able to
/// successfully start (but not bootstrap) an ssh connection with the given
/// remote shell. Verifies that the ssh connection worked by asserting that
/// there is still a long-running block after entering the password, and that
/// attempting to run `exit` returns us to the bootstrapped local shell.
macro_rules! generate_long_running_block_ssh_test_for_shell {
($fn_name:ident, $shell:literal, prompt_regex: $prompt_regex:literal) => {
/// Ensure we can successfully ssh into a $shell remote shell and bootstrap it
/// successfully.
pub fn $fn_name() -> Builder {
new_builder()
// TODO(CORE-2333) PowerShell has no SSH wrapper.
.set_should_run_test(|| {
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(setup_gcloud_sdk())
.with_step(enter_ssh_command($shell))
.with_step(wait_for_password_prompt(0 /*tab_idx*/, $shell))
.with_step(enter_ssh_password())
.with_step(
TestStep::new("Assert prompt is awaiting input")
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
)
.add_assertion(move |app, window_id| {
let regex = Regex::new($prompt_regex)
.expect("regex should not fail to compile");
validate_block_output(&regex, 0, 0, window_id, app)
}),
)
.with_step(verify_login_shell($shell))
.with_step(TestStep::new("Exit ssh session").with_typed_characters(&["exit\n"]))
.with_step(new_step_with_default_assertions(
"Assert ssh session has completed",
))
}
};
}
// Generate test methods to validate expected ssh behavior for a variety of
// remote shells.
generate_can_bootstrap_legacy_ssh_test_for_shell!(test_legacy_ssh_into_bash, "bash");
generate_can_bootstrap_legacy_ssh_test_for_shell!(test_legacy_ssh_into_zsh, "zsh");
generate_can_bootstrap_tmux_ssh_test_for_shell!(test_tmux_ssh_into_bash, "bash", false);
generate_can_bootstrap_tmux_ssh_test_for_shell!(test_tmux_ssh_into_zsh, "zsh", false);
generate_can_bootstrap_tmux_ssh_test_for_shell!(test_install_tmux_ssh_into_bash, "bash", true);
generate_can_bootstrap_tmux_ssh_test_for_shell!(test_install_tmux_ssh_into_zsh, "zsh", true);
generate_long_running_block_ssh_test_for_shell!(test_ssh_into_fish, "fish", prompt_regex: r"\nfish@ubuntu-14-04 ~>$");
generate_long_running_block_ssh_test_for_shell!(test_ssh_into_sh, "sh", prompt_regex: r"\n\$ $");
generate_long_running_block_ssh_test_for_shell!(test_ssh_into_ash, "ash", prompt_regex: r"\n\$ $");
/// Tests a regression with the startup shell setting and SSH proxies.
/// See WAR-6337 for details - if `$SHELL` is not set to a valid executable file
/// path, SSH fails to execute proxy commands (like the one this test uses for
/// gcloud).
pub fn test_ssh_with_shell_override() -> Builder {
new_builder()
// TODO(CORE-2333) PowerShell has no SSH wrapper.
.set_should_run_test(|| {
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.with_user_defaults(HashMap::from([(
StartupShellOverride::storage_key().to_owned(),
serde_json::to_string(&StartupShell::Zsh).expect("Can serialize setting as JSON"),
)]))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(setup_gcloud_sdk())
.with_step(enter_ssh_command("bash"))
.with_step(wait_for_password_prompt(0, "bash"))
.with_step(enter_ssh_password())
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Assert active block is part of a remote session")
.add_assertion(assert_active_block_is_remote("bash", "ubuntu-14-04")),
)
.with_step(verify_login_shell("bash"))
}
+141
View File
@@ -0,0 +1,141 @@
use std::collections::HashMap;
use settings::Setting as _;
use warp::integration_testing::terminal::util::current_shell_starter_and_version;
use warp::integration_testing::view_getters::single_input_view_for_tab;
use warp::root_view::SubshellCommandArg;
use warp::terminal::shell::ShellType;
use warp::{
integration_testing::{
step::new_step_with_default_assertions,
subshell::{
assert_subshell_banner_is_showing, assert_subshell_is_bootstrapped,
enter_local_subshell_command, enter_remote_subshell_command, enter_ssh_password,
setup_gcloud_sdk, trigger_subshell_bootstrap, util::ssh_command,
wait_for_password_prompt,
},
terminal::wait_until_bootstrapped_single_pane_for_tab,
},
terminal::warpify::settings::AddedSubshellCommands,
};
use warpui::integration::{AssertionOutcome, TestStep};
use warpui::windowing::state::ApplicationStage;
use warpui::windowing::WindowManager;
use warpui::{async_assert, UpdateModel};
use crate::util::skip_if_powershell_core_2303;
use super::{new_builder, Builder};
/// Generates an integration test that asserts that a local subshell of the given shell type can be
/// successfully bootstrapped.
macro_rules! generate_can_bootstrap_local_subshell_for_shell {
($fn_name:ident, $shell:literal) => {
/// Ensure a local subshell bootstraps successfully.
pub fn $fn_name() -> Builder {
new_builder()
// We've noticed that these tests sometimes fail due to not
// cleaning up files after the test, so we use a temp dir
// to hedge against this.
.use_tmp_filesystem_for_test_root_directory()
// TODO(CORE-2730): Re-enable once powershell has subshell support
.set_should_run_test(skip_if_powershell_core_2303)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(enter_local_subshell_command($shell))
.with_step(assert_subshell_banner_is_showing())
.with_step(trigger_subshell_bootstrap())
.with_step(assert_subshell_is_bootstrapped(0, 0))
}
};
}
generate_can_bootstrap_local_subshell_for_shell!(test_can_bootstrap_local_bash_subshell, "bash");
generate_can_bootstrap_local_subshell_for_shell!(test_can_bootstrap_local_fish_subshell, "fish");
generate_can_bootstrap_local_subshell_for_shell!(test_can_bootstrap_local_zsh_subshell, "zsh");
macro_rules! generate_can_bootstrap_remote_subshell_for_shell {
($fn_name:ident, $shell:literal) => {
/// Ensure a local subshell bootstraps successfully.
pub fn $fn_name() -> Builder {
new_builder()
// TODO(CORE-2333) PowerShell has no SSH wrapper.
.set_should_run_test(|| {
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() != ShellType::PowerShell
})
.with_user_defaults(HashMap::from([(
AddedSubshellCommands::storage_key().to_owned(),
serde_json::to_string(&vec![ssh_command($shell, false)])
.expect("Can serialize Vec<String> to string"),
)]))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(setup_gcloud_sdk())
.with_step(enter_remote_subshell_command($shell))
.with_step(wait_for_password_prompt(0 /*tab_idx*/, $shell))
.with_step(
enter_ssh_password().set_post_step_pause(std::time::Duration::from_millis(250)),
)
.with_step(trigger_subshell_bootstrap())
.with_step(assert_subshell_is_bootstrapped(0, 0))
}
};
}
generate_can_bootstrap_remote_subshell_for_shell!(test_can_bootstrap_remote_zsh_subshell, "zsh");
generate_can_bootstrap_remote_subshell_for_shell!(test_can_bootstrap_remote_bash_subshell, "bash");
// TODO(CORE-348): Consider upgrading the fish version in the testing VM so we can enable this
// test.
// generate_can_bootstrap_remote_subshell_for_shell!(test_can_bootstrap_remote_fish_subshell, "fish");
// Test the flow of creating a new window and running a command that should create a subshell and
// automaticall bootstrapping AKA "warpifying" that subshell.
pub fn test_can_auto_bootstrap() -> Builder {
const SUBSHELL_COMMAND: &str = "zsh";
new_builder()
// TODO(CORE-2730): Re-enable once powershell has subshell support
.set_should_run_test(skip_if_powershell_core_2303)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(TestStep::new("foo").add_assertion(|app, window_id| {
app.update_model(
&app.get_singleton_model_handle::<WindowManager>(),
|window_manager, _| {
window_manager.overwrite_for_test(ApplicationStage::Active, Some(window_id));
AssertionOutcome::Success
},
)
}))
.with_step(
new_step_with_default_assertions("Insert subshell command in new tab").with_action(
move |app, _, _| {
app.dispatch_global_action(
"root_view:open_new_tab_insert_subshell_command_and_bootstrap_if_supported",
SubshellCommandArg {
command: SUBSHELL_COMMAND.to_owned(),
shell_type: Some(ShellType::Zsh),
},
);
},
),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(
new_step_with_default_assertions("Check that subshell command was inserted")
.add_named_assertion(
"Check that input buffer contains the subshell command",
|app, window_id| {
let input_view = single_input_view_for_tab(app, window_id, 1);
input_view.read(app, |input, ctx| {
async_assert!(
input.buffer_text(ctx) == SUBSHELL_COMMAND,
"Subshell command was not inserted"
)
})
},
),
)
.with_step(
new_step_with_default_assertions("run subshell command").with_keystrokes(&["enter"]),
)
.with_step(assert_subshell_is_bootstrapped(1, 0))
}
+299
View File
@@ -0,0 +1,299 @@
use warp::{
cmd_or_ctrl_shift,
integration_testing::{
step::new_step_with_default_assertions,
terminal::{
assert_active_block_output, assert_command_executed,
assert_long_running_block_executing, assert_no_block_executing, execute_command,
run_alt_grid_program, util::ExpectedExitStatus, wait_until_bootstrapped_pane,
wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::{terminal_view, workspace_view},
},
workspace::WorkspaceAction,
};
use warpui::{async_assert, async_assert_eq, integration::TestStep};
use crate::util::{get_input_buffer, skip_if_powershell_core_2303};
use super::{new_builder, Builder};
pub fn test_input_syncing_is_off_by_default() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("create one additional pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
new_step_with_default_assertions(
"type something into pane 2 and check both pane contents",
)
.with_keystrokes(&["b"])
.add_named_assertion("Check that pane 1 is still empty", |app, window_id| {
let input1 = get_input_buffer(app, window_id, 0, 0);
async_assert!(
input1.is_empty(),
"pane 1 should be empty but it contains {}",
input1
)
})
.add_named_assertion(
"Check that pane 2 has the correct contents",
|app, window_id| {
let input2 = get_input_buffer(app, window_id, 0, 1);
async_assert!(
input2 == "b",
"pane 2 should contain 'b' but it contains {}",
input2
)
},
),
)
}
pub fn test_can_sync_input_editor_text_in_tab() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("create one additional pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
new_step_with_default_assertions("turn syncing on in tab").with_action(
move |app, _, _| {
let window_id =
app.read(|ctx| ctx.windows().active_window().expect("no active window"));
let workspace_view_id = workspace_view(app, window_id).id();
app.dispatch_typed_action(
window_id,
&[workspace_view_id],
&WorkspaceAction::ToggleSyncTerminalInputsInTab,
);
},
),
)
.with_step(
new_step_with_default_assertions(
"type something into pane 2 and check pane 1 and 2 contents",
)
.with_keystrokes(&["b"])
.add_named_assertion(
"check that pane 1 and 2 have the same contents",
|app, window_id| {
let input1 = get_input_buffer(app, window_id, 0, 0);
let input2 = get_input_buffer(app, window_id, 0, 1);
async_assert!(
input1 == "b" && input2 == "b",
"Both panes should contain 'b', pane 1: '{input1}', pane 2: '{input2}'"
)
},
),
)
}
pub fn test_can_run_command_in_synced_panes_in_tab() -> Builder {
let command = "echo typedInPane2";
let expected_output = "typedInPane2";
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(
new_step_with_default_assertions("create one additional pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
new_step_with_default_assertions("turn syncing on in tab").with_action(
move |app, _, _| {
let window_id =
app.read(|ctx| ctx.windows().active_window().expect("no active window"));
let workspace_view_id = workspace_view(app, window_id).id();
app.dispatch_typed_action(
window_id,
&[workspace_view_id],
&WorkspaceAction::ToggleSyncTerminalInputsInTab,
);
},
),
)
.with_step(
execute_command(
0,
0,
command.to_owned(),
ExpectedExitStatus::Success,
expected_output,
)
.add_named_assertion(
"assert that the same command ran in pane 0",
assert_command_executed(0, 1, command.to_owned()),
),
)
}
pub fn test_synced_panes_long_running_commands() -> 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(
new_step_with_default_assertions("create one additional pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
new_step_with_default_assertions("turn syncing on in tab").with_action(
move |app, _, _| {
let window_id =
app.read(|ctx| ctx.windows().active_window().expect("no active window"));
let workspace_view_id = workspace_view(app, window_id).id();
app.dispatch_typed_action(
window_id,
&[workspace_view_id],
&WorkspaceAction::ToggleSyncTerminalInputsInTab,
);
},
),
)
.with_step(
TestStep::new("Execute sleep 1000000 in both panes")
.with_typed_characters(&["sleep 1000000"])
.with_keystrokes(&["enter"])
.add_named_assertion(
"check that sleep 1000000 ran in pane 0",
assert_long_running_block_executing(true, 0, 0),
)
.add_named_assertion(
"check that sleep 1000000 ran in pane 1",
assert_long_running_block_executing(true, 0, 1),
),
)
.with_step(
TestStep::new("Send text to both panes")
.with_typed_characters(&["foo"])
.add_named_assertion(
"check that foo was sent to pane 0",
assert_active_block_output("foo", 0, 0),
)
.add_named_assertion(
"check that foo was sent to pane 1",
assert_active_block_output("foo", 0, 1),
),
)
.with_step(
TestStep::new("Exit sleep 1000000 in both panes")
.with_keystrokes(&["ctrl-c"])
.add_named_assertion(
"check that no command is running in pane 0",
assert_no_block_executing(0, 0),
)
.add_named_assertion(
"check that no command is running in pane 1",
assert_no_block_executing(0, 1),
),
)
}
/// Tests that as you use synced inputs and terminals switch between
/// alt-screens and the block-list, the correct terminal view maintains focus.
pub fn test_synced_inputs_terminal_mode_change_view_focus() -> Builder {
let mut 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));
for i in 1..=3 {
builder = builder
.with_step(
new_step_with_default_assertions(format!("create pane {i} in tab 0").as_str())
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, i));
}
builder = builder.with_step(
new_step_with_default_assertions("create 2nd tab")
.with_keystrokes(&[cmd_or_ctrl_shift("t")]),
);
for i in 1..=3 {
builder = builder
.with_step(
new_step_with_default_assertions(format!("create pane {i} in tab 1").as_str())
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(1, i));
}
builder = builder.with_step(
new_step_with_default_assertions("turn syncing on across all tabs").with_action(
move |app, _, _| {
let window_id =
app.read(|ctx| ctx.windows().active_window().expect("no active window"));
let workspace_view_id = workspace_view(app, window_id).id();
app.dispatch_typed_action(
window_id,
&[workspace_view_id],
&WorkspaceAction::ToggleSyncAllTerminalInputsInAllTabs,
);
},
),
);
let exit_vim_step = TestStep::new("Close vim")
.with_keystrokes(&["escape"])
.with_typed_characters(&[":q!"])
.with_keystrokes(&["enter"]);
let vim_steps = run_alt_grid_program(
"vim",
1,
3,
exit_vim_step,
vec![
TestStep::new("While vim is running, check focused terminal").add_named_assertion(
"tab 1 terminal 3 is focused",
|app, window_id| {
let terminal_view_id = terminal_view(app, window_id, 1, 3).id();
app.update(|app_ctx| {
async_assert_eq!(
app_ctx.check_view_or_child_focused(window_id, &terminal_view_id),
true
)
})
},
),
],
);
builder = builder.with_steps(vim_steps);
builder = builder.with_step(
TestStep::new("check focused terminal after exiting vim").add_named_assertion(
"tab 1 terminal 3 is focused",
|app, window_id| {
let terminal_view_id = terminal_view(app, window_id, 1, 3).id();
app.update(|app_ctx| {
async_assert_eq!(
app_ctx.check_view_or_child_focused(window_id, &terminal_view_id),
true
)
})
},
),
);
builder
}
+326
View File
@@ -0,0 +1,326 @@
use warp::{
integration_testing::{
agent_mode::AgentViewState,
step::new_step_with_default_assertions,
terminal::{
assert_active_block_output_for_single_terminal_in_tab, assert_input_editor_contents,
assert_long_running_block_executing_for_single_terminal_in_tab,
assert_no_visible_background_blocks, util::current_shell_starter_and_version,
wait_until_bootstrapped_single_pane_for_tab,
},
view_getters::single_terminal_view_for_tab,
},
terminal::{
model::terminal_model::BlockIndex,
shell::{Shell, ShellType},
},
};
use warpui::{
async_assert, async_assert_eq,
integration::{AssertionCallback, AssertionOutcome, TestStep},
};
use crate::util::skip_if_powershell_core_2303;
use super::{new_builder, Builder};
pub fn test_typeahead() -> 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(
TestStep::new("Execute sleep 4")
.with_typed_characters(&["sleep 4"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(
TestStep::new("Enter text to long running command")
.with_input_string("foo", None)
.add_assertion(require_long_running_block_executing(0))
.add_assertion(assert_active_block_output_for_single_terminal_in_tab(
"foo", 0,
)),
)
.with_step(
new_step_with_default_assertions("Input box should have typeahead text")
.add_assertion(assert_input_editor_contents(0, "foo"))
.add_named_assertion(
"No typeahead duplicated in background block",
assert_no_visible_background_blocks(0, 0),
),
)
}
/// Checks a typeahead command has the expected value.
///
/// There's a race condition sending the ESC-i keybinding to
/// report input. In real user input, it's unlikely, but it
/// happens in integration tests because of how quickly the
/// command is entered.
macro_rules! check_command {
($command:expr, $expected:expr) => {
let command = $command;
if command.contains("^[i") {
return AssertionOutcome::PreconditionFailed(format!(
"Flake: input reporting keybinding sent too early on `{command}`"
));
} else {
assert_eq!(command, $expected);
}
};
}
/// Tests that the shell reports its input buffer to the Warp typeahead model after
/// a long-running command completes.
pub fn test_input_reporting_posix_shells() -> Builder {
// When the shell can report its input buffer, we can handle typeahead with
// line editing. When matching user input ourselves (only on pre-4.0 bash),
// we do not support line edits.
let (starter, version) = current_shell_starter_and_version();
let shell = Shell::new(
starter.shell_type(),
Some(version),
None,
Default::default(),
None,
);
let supports_line_editing = shell.input_reporting_sequence().is_some();
let mut input_step = TestStep::new("Enter text to long-running command")
.with_input_string("true", Some(&["enter"]))
// Test behavior when one of the typeahead commands is itself long-running.
.with_input_string("sleep 1", Some(&["enter"]))
.add_assertion(require_long_running_block_executing(0));
if supports_line_editing {
// Test that we correctly handle line edits on both submitted lines and typeahead.
input_step = input_step
.with_keystrokes(&["p", "w", "f", "backspace", "d", "enter"])
.with_keystrokes(&["l", "s", " ", "-", "a", "backspace", "l"]);
} else {
input_step = input_step
.with_input_string("pwd", Some(&["enter"]))
// This is the input we expect as typeahead.
.with_input_string("ls -l", None);
}
new_builder()
.set_should_run_test(move || starter.shell_type() != ShellType::PowerShell)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute sleep")
.with_typed_characters(&["sleep 3"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(input_step)
.with_step(
new_step_with_default_assertions("Input should be reported to the terminal")
.add_named_assertion(
"Typeahead is in input editor",
assert_input_editor_contents(0, "ls -l"),
)
.add_named_assertion("Intermediate commands ran", |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let blocks = model.block_list();
let start_index = blocks
.first_non_hidden_block_by_index()
.expect("Block should exist");
let sleep_3_block =
blocks.block_at(start_index).expect("Block should exist");
check_command!(sleep_3_block.command_to_string(), "sleep 3");
let true_block = blocks
.block_at(start_index + BlockIndex::from(1))
.expect("Block should exist");
assert!(!true_block.is_background());
check_command!(true_block.command_to_string(), "true");
let sleep_1_block = blocks
.block_at(start_index + BlockIndex::from(2))
.expect("Block should exist");
assert!(!sleep_1_block.is_background());
check_command!(sleep_1_block.command_to_string(), "sleep 1");
let pwd_block = blocks
.block_at(start_index + BlockIndex::from(3))
.expect("Block should exist");
assert!(!pwd_block.is_background());
check_command!(pwd_block.command_to_string(), "pwd");
// On shells that support input reporting, there will be
// an empty block that formerly held echoed typeahead. On
// shells using input matching, the typeahead block is never
// created.
let next_block = blocks
.block_at(start_index + BlockIndex::from(4))
.expect("Block should exist");
if next_block.is_background() {
async_assert!(next_block.is_empty(&AgentViewState::Inactive))
} else {
async_assert_eq!(next_block.index(), blocks.active_block_index())
}
})
}),
)
}
/// PowerShell has different behavior for typeahead in that it ignores newlines.
pub fn test_input_reporting_powershell() -> Builder {
new_builder()
.set_should_run_test(|| {
let (starter, _) = current_shell_starter_and_version();
starter.shell_type() == ShellType::PowerShell
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Execute sleep")
.with_typed_characters(&["sleep 3"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 0),
),
)
.with_step(
TestStep::new("Enter text to long-running command")
.with_keystrokes(&["enter"])
.with_input_string("true", Some(&["enter"]))
.with_input_string("sleep 1", Some(&["enter"]))
.add_assertion(require_long_running_block_executing(0)),
)
.with_step(
new_step_with_default_assertions("Input should be reported to the terminal")
.add_named_assertion(
"Typeahead is in input editor",
assert_input_editor_contents(0, "truesleep 1"),
),
)
}
/// This tests UNIX-specific signal handling.
#[cfg(not(windows))]
pub fn test_background_output() -> Builder {
use regex::Regex;
use std::{fs::OpenOptions, io::Write, os::unix::prelude::OpenOptionsExt};
use warp::integration_testing::{
block::assert_background_output,
terminal::{execute_command_for_single_terminal_in_tab, util::ExpectedExitStatus},
};
let (starter, _) = current_shell_starter_and_version();
let (spawn_command, kill_command) = match starter.shell_type() {
ShellType::PowerShell => (
"$process = Start-Process -FilePath './delayed_output.py' -PassThru",
"kill -SIGUSR1 $process.Id && echo foreground",
),
_ => (
"./delayed_output.py &",
"kill -SIGUSR1 %1 && echo foreground",
),
};
new_builder()
.with_setup(|utils| {
let dir = utils.test_dir();
// Use a Python script because fish can't run functions in the background
// https://github.com/fish-shell/fish-shell/issues/238
let script_path = dir.join("delayed_output.py");
OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o755)
.open(script_path)
.expect("could not create script")
.write_all(
br#"#!/usr/bin/env python3
import signal
import time
# Wait for a SIGUSR1 signal, after which we should print out
# more text.
def handler(signo, cur_frame):
time.sleep(1)
print("Output 2")
print("Output 3")
signal.signal(signal.SIGUSR1, handler)
print("Output 1")
time.sleep(100)
"#,
)
.expect("could not write Python script");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(execute_command_for_single_terminal_in_tab(
0,
spawn_command.into(),
ExpectedExitStatus::Success,
(),
))
.with_step(
TestStep::new("First line of background output appears")
.add_assertion(assert_background_output(0, "Output 1\n")),
)
.with_step(execute_command_for_single_terminal_in_tab(
0,
// Send the signal to the background process and produce some output.
kill_command.into(),
ExpectedExitStatus::Success,
"foreground",
))
.with_step(
TestStep::new("Rest of background output appears in a new block").add_assertion(
assert_background_output(
0,
// Use a regex because the "job completed" message format is shell-specific.
// Depending on timing, the "Output 2" line could be part of the
// block for `true`, so it's optional - we expect the next
// line to always be in the background block though.
Regex::new("^(Output 2\n)?Output 3\n").expect("Regex is valid"),
),
),
)
}
#[cfg(windows)]
// TODO(CORE-2302): enable this test for windows
pub fn test_background_output() -> Builder {
new_builder()
}
/// Require (as a test precondition) that a long-running block is executing.
/// Use this instead of [`assert_long_running_block_executing`] with `sleep` commands
/// to turn the race condition of the sleep ending too soon into a flake.
fn require_long_running_block_executing(tab_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, tab_index);
terminal_view.read(app, |view, _ctx| {
// The running block should have received the text.
let model = view.model.lock();
if !model
.block_list()
.active_block()
.is_active_and_long_running()
{
// There's an implicit race condition where the sleep call
// can finish before we get here.
// The most robust way of handling this is to just treat it as a flake.
AssertionOutcome::PreconditionFailed(
"long-running block no longer executing".to_owned(),
)
} else {
AssertionOutcome::Success
}
})
})
}
@@ -0,0 +1,212 @@
use std::future::Future;
use std::pin::Pin;
use pathfinder_geometry::vector::vec2f;
use warpui::event::{Event, ModifiersState};
use warpui::integration::{TestStep, ARTIFACTS_DIR_ENV_VAR};
use crate::Builder;
use warp::integration_testing::step::new_step_with_default_assertions;
use warp::integration_testing::terminal::util::ExpectedExitStatus;
use warp::integration_testing::terminal::{
assert_view_has_text_selection, clear_blocklist_to_remove_bootstrapped_blocks,
execute_command_for_single_terminal_in_tab, execute_echo_str,
wait_until_bootstrapped_single_pane_for_tab,
};
/// Exercises the video recording, screenshot, and overlay annotation APIs.
///
/// This test is meant to be run manually with a real display to verify
/// that frame capture, video encoding, overlay compositing, and artifact
/// export all work end-to-end:
///
/// ```sh
/// WARPUI_USE_REAL_DISPLAY_IN_INTEGRATION_TESTS=1 \
/// cargo run -p integration --bin integration -- test_video_recording
/// ```
///
/// The recording exercises every overlay type:
/// - Mouse click indicators (filled dot + expanding ring)
/// - Drag trails (selecting terminal output text)
/// - Key sequence pills (typing commands, Ctrl-C, Cmd-A / Cmd-C)
pub fn test_video_recording() -> Builder {
Builder::new()
.with_real_display()
.with_on_finish(
move |_app, _window_id, _persisted_data| -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async move {
let artifacts_root = std::env::var(ARTIFACTS_DIR_ENV_VAR)
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
std::env::temp_dir().join("warp_integration_test_artifacts")
});
let test_dir = artifacts_root.join("test_video_recording");
let latest_run = std::fs::read_dir(&test_dir).ok().and_then(|entries| {
entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.max_by_key(|entry| entry.file_name())
.map(|entry| entry.path())
});
let Some(run_dir) = latest_run else {
panic!(
"No timestamped run directory found under {}",
test_dir.display()
);
};
let bootstrap_png = run_dir.join("after_bootstrap.png");
let commands_png = run_dir.join("after_commands.png");
let video_mp4 = run_dir.join("recording.mp4");
let log_file = run_dir.join("recording.log");
assert!(
bootstrap_png.exists(),
"Expected after_bootstrap.png in {}",
run_dir.display()
);
assert!(
commands_png.exists(),
"Expected after_commands.png in {}",
run_dir.display()
);
assert!(
video_mp4.exists(),
"Expected recording.mp4 in {}",
run_dir.display()
);
assert!(
log_file.exists(),
"Expected recording.log in {}",
run_dir.display()
);
let video_size = std::fs::metadata(&video_mp4)
.map(|metadata| metadata.len())
.unwrap_or(0);
assert!(
video_size > 1000,
"recording.mp4 is suspiciously small ({video_size} bytes)"
);
log::info!(
"All artifacts verified in {}. recording.mp4 = {} bytes",
run_dir.display(),
video_size
);
})
},
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(clear_blocklist_to_remove_bootstrapped_blocks())
.with_step(
TestStep::new("Take screenshot after bootstrap")
.with_take_screenshot("after_bootstrap.png"),
)
.with_step(TestStep::new("Start recording").with_start_recording())
.with_step(execute_echo_str(0, "hello from the video test"))
.with_step(execute_echo_str(0, "second line of output"))
.with_step(execute_command_for_single_terminal_in_tab(
0,
"ls".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(
TestStep::new("Click in terminal")
.with_event(Event::LeftMouseDown {
position: vec2f(300.0, 250.0),
modifiers: ModifiersState::default(),
click_count: 1,
is_first_mouse: false,
})
.with_event(Event::LeftMouseUp {
position: vec2f(300.0, 250.0),
modifiers: ModifiersState::default(),
}),
)
.with_step(
new_step_with_default_assertions("Close left panel")
.with_click_on_saved_position("workspace:toggle_left_panel"),
)
.with_step(
new_step_with_default_assertions("Start drag-select")
.with_event_fn(|app, window_id| {
let presenter = app.presenter(window_id).expect("presenter");
let bounds = presenter
.borrow()
.position_cache()
.get_position("block_index:0")
.expect("block_index:0 position");
Event::LeftMouseDown {
position: bounds.origin(),
modifiers: ModifiersState::default(),
click_count: 1,
is_first_mouse: false,
}
})
.with_event_fn(|app, window_id| {
let presenter = app.presenter(window_id).expect("presenter");
let b1 = presenter
.borrow()
.position_cache()
.get_position("block_index:1")
.expect("block_index:1 position");
Event::LeftMouseDragged {
position: b1.center(),
modifiers: ModifiersState::default(),
}
})
.with_event_fn(|app, window_id| {
let presenter = app.presenter(window_id).expect("presenter");
let b1 = presenter
.borrow()
.position_cache()
.get_position("block_index:1")
.expect("block_index:1 position");
Event::LeftMouseDragged {
position: b1.lower_right(),
modifiers: ModifiersState::default(),
}
})
.add_assertion(assert_view_has_text_selection(true)),
)
.with_step(
new_step_with_default_assertions("End drag-select")
.with_event_fn(|app, window_id| {
let presenter = app.presenter(window_id).expect("presenter");
let b1 = presenter
.borrow()
.position_cache()
.get_position("block_index:1")
.expect("block_index:1 position");
Event::LeftMouseUp {
position: b1.lower_right(),
modifiers: ModifiersState::default(),
}
})
.add_assertion(assert_view_has_text_selection(false)),
)
.with_step(TestStep::new("Copy selection").with_keystrokes(&["cmd-c"]))
.with_step(
TestStep::new("Type text for ctrl editing").with_input_string("hello world", None),
)
.with_step(
TestStep::new("Ctrl-A, Ctrl-E, Ctrl-U")
.with_keystrokes(&["ctrl-a", "ctrl-e", "ctrl-u"]),
)
.with_step(execute_command_for_single_terminal_in_tab(
0,
"echo 'video recording test complete'".to_string(),
ExpectedExitStatus::Success,
(),
))
.with_step(TestStep::new("Select all").with_keystrokes(&["cmd-a"]))
.with_step(TestStep::new("Stop recording").with_stop_recording())
.with_step(
TestStep::new("Take screenshot after commands")
.with_take_screenshot("after_commands.png"),
)
}
+51
View File
@@ -0,0 +1,51 @@
use warp::integration_testing::{
self,
assertions::{
assert_websocket_has_not_started, assert_websocket_has_started, create_a_personal_workflow,
join_a_workspace,
},
terminal::wait_until_bootstrapped_single_pane_for_tab,
};
use crate::Builder;
use super::{new_builder, TEST_ONLY_ASSETS};
/// With no objects and no teams, the websocket should not begin
pub fn test_websocket_does_not_begin_on_startup() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(assert_websocket_has_not_started())
}
/// With objects read from sqlite (i.e., objects that are not welcome objects), the websocket should begin
pub fn test_websocket_begins_on_startup() -> Builder {
new_builder()
.with_setup(|_utils| {
integration_testing::create_file_from_assets(
TEST_ONLY_ASSETS,
"cloud_objects.sqlite",
&integration_testing::persistence::database_file_path(),
);
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(assert_websocket_has_started())
}
/// The websocket should start only after joining a team
pub fn test_websocket_begins_after_joining_a_team() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(assert_websocket_has_not_started())
.with_step(join_a_workspace())
.with_step(assert_websocket_has_started())
}
/// The websocket should start only after an object is created
pub fn test_websocket_begins_after_creating_an_object() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(assert_websocket_has_not_started())
.with_step(create_a_personal_workflow())
.with_step(assert_websocket_has_started())
}
+153
View File
@@ -0,0 +1,153 @@
use std::time::Duration;
use warp::integration_testing::workflow::{
assert_no_team_workflow_pane_open, assert_open_team_workflow_pane_count_equals,
};
use warp::{
integration_testing::{
self,
assertions::{go_offline, go_online, join_a_workspace},
command_palette::{open_command_palette_and_run_action, TestStepsExt},
step::new_step_with_default_assertions,
terminal::{
execute_command_for_single_terminal_in_tab, util::ExpectedExitStatus,
wait_until_bootstrapped_single_pane_for_tab,
},
view_of_type,
window::save_active_window_id,
workflow::{
assert_no_workflow_pane_open, assert_open_workflow_pane_count_equals,
assert_workflow_id, create_a_personal_workflow, open_workflow,
},
},
workflows::CategoriesView,
};
use warpui::{async_assert_eq, integration::TestStep, ViewHandle};
use crate::Builder;
use super::{new_builder, TEST_ONLY_ASSETS};
pub fn test_open_workflow_in_pane() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
create_a_personal_workflow("workflow_2_key")
.add_assertion(save_active_window_id("first window")),
)
.with_step(
open_workflow("first window", "workflow_2_key")
.add_named_assertion_with_data_from_prior_step(
"Verify workflow is open",
assert_workflow_id(0, 0, "workflow_2_key"),
),
)
}
pub fn test_create_personal_workflow_pane_from_command_palette() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(TestStep::new("Noop step").add_named_assertion(
"Make sure no workflow panes are open",
assert_no_workflow_pane_open(),
))
.with_steps(
open_command_palette_and_run_action("Create a New Personal Workflow")
.add_named_assertion(
"There should be one workflow pane open",
assert_open_workflow_pane_count_equals(1),
),
)
}
pub fn test_create_team_workflow_pane_from_command_palette() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(TestStep::new("Noop step").add_named_assertion(
"Make sure no workflow panes are open",
assert_no_workflow_pane_open(),
))
.with_step(join_a_workspace())
.with_step(go_offline())
.with_step(
TestStep::new("delay for test consistency")
.set_post_step_pause(Duration::from_millis(250)),
)
.with_steps(
open_command_palette_and_run_action("Create a New Team Workflow").add_named_assertion(
"There should still not be any panes open",
assert_no_team_workflow_pane_open(),
),
)
.with_step(go_online())
.with_steps(
open_command_palette_and_run_action("Create a New Team Workflow").add_named_assertion(
"There should be an open workflow pane",
assert_open_team_workflow_pane_count_equals(1),
),
)
}
/// Adds a workflow file, containing two workflows, to a `.warp/workflows`
/// directory under a git repository and verifies that the workflows appear
/// in the workflow menu.
pub fn test_loading_project_workflows() -> Builder {
new_builder()
.with_setup(move |utils| {
utils.set_env("WARP_CONFIG_WATCHER_DELAY_MS", Some((10).to_string()));
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Should have no project workflows").add_named_assertion(
"Should have no project workflows",
|app, window_id| {
let workflows: ViewHandle<CategoriesView> = view_of_type(app, window_id, 0);
workflows.read(app, |workflows, _| {
// Note that this can be a synchronous assertion because unlike the next assertion,
// we don't have concurrency with a WarpConfig watcher thread
async_assert_eq!(
workflows.project_workflows().count(),
0,
"There should not be any project workflows"
)
})
},
),
)
// Create a git repository in the `repo` subdirectory.
.with_step(execute_command_for_single_terminal_in_tab(
0,
"git init repo && cd repo".into(),
ExpectedExitStatus::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",
&utils
.test_dir()
.join("repo/.warp/workflows/test_workflow.yaml"),
);
}),
)
.with_step(
new_step_with_default_assertions(
"Open the workflows browser to refresh the list of project workflows",
)
.with_keystrokes(&["ctrl-shift-R"]),
)
.with_step(
TestStep::new("Verify the workflows were loaded successfully").add_named_assertion(
"The two added workflows should be in the view",
|app, window_id| {
let workflows: ViewHandle<CategoriesView> = view_of_type(app, window_id, 0);
let num_workflows =
workflows.read(app, |workflows, _| workflows.project_workflows().count());
async_assert_eq!(num_workflows, 2)
},
),
)
}
+179
View File
@@ -0,0 +1,179 @@
//! Integration tests for workspace-level behavior.
use std::fs;
use settings::Setting as _;
use warp::integration_testing::terminal::assert_long_running_block_executing_for_single_terminal_in_tab;
use warp::integration_testing::view_getters::terminal_view;
use warp::integration_testing::workspace::{assert_tab_count, press_native_modal_button};
use warp::{
cmd_or_ctrl_shift,
integration_testing::{
pane_group::assert_focused_pane_index,
step::new_step_with_default_assertions,
terminal::{
assert_active_session_local_path, execute_command, util::ExpectedExitStatus,
wait_until_bootstrapped_pane, wait_until_bootstrapped_single_pane_for_tab,
},
},
settings::PaneSettings,
workspace::NEW_TAB_BUTTON_POSITION_ID,
};
use warpui::{async_assert, integration::TestStep, SingletonEntity};
use crate::{util::skip_if_powershell_core_2303, Builder};
use super::new_builder;
pub fn test_active_session_follows_focus() -> Builder {
new_builder()
// TODO(CORE-2732): Flakey on Powershell (Linux)
.set_should_run_test(skip_if_powershell_core_2303)
.with_setup(|utils| {
fs::create_dir(utils.test_dir().join("dir1")).expect("Couldn't create subdirectory");
fs::create_dir(utils.test_dir().join("dir2")).expect("Couldn't create subdirectory");
})
.with_step(
new_step_with_default_assertions("Ensure initial active session is set")
.add_assertion(assert_active_session_local_path("~")),
)
.with_step(
new_step_with_default_assertions("Create another session in the same tab")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
execute_command(0, 1, "cd dir1".to_string(), ExpectedExitStatus::Success, ())
.add_assertion(assert_active_session_local_path("~/dir1")),
)
.with_step(
new_step_with_default_assertions("Switch to the first session")
.with_keystrokes(&["cmdorctrl-meta-left"])
.add_assertion(assert_active_session_local_path("~")),
)
.with_step(
new_step_with_default_assertions("Open a new tab")
.with_keystrokes(&[cmd_or_ctrl_shift("t")]),
)
.with_step(wait_until_bootstrapped_pane(1, 0))
.with_step(
execute_command(1, 0, "cd dir2".to_string(), ExpectedExitStatus::Success, ())
.add_assertion(assert_active_session_local_path("~/dir2")),
)
.with_step(
new_step_with_default_assertions("Switch to the first tab")
.with_keystrokes(&["cmdorctrl-1"])
.add_assertion(assert_active_session_local_path("~")),
)
.with_step(
new_step_with_default_assertions("Close the tab")
.with_keystrokes(&[cmd_or_ctrl_shift("w"), cmd_or_ctrl_shift("w")])
.add_assertion(assert_active_session_local_path("~/dir2")),
)
}
pub fn test_focus_panes_on_hover() -> Builder {
new_builder()
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Create a new session in a split pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")])
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(wait_until_bootstrapped_pane(0, 1))
.with_step(
new_step_with_default_assertions("Enable focus pane on hover").add_assertion(
|app, _| {
PaneSettings::handle(app).update(app, |settings, ctx| {
settings
.focus_panes_on_hover
.set_value(true, ctx)
.expect("error updating setting");
async_assert!(*settings.focus_panes_on_hover)
})
},
),
)
.with_step(
new_step_with_default_assertions("Hover over the initial pane's terminal")
.with_hover_on_saved_position_fn(|app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
terminal_view.read(app, |terminal, _| terminal.terminal_position_id())
})
.add_assertion(assert_focused_pane_index(0, 0)),
)
.with_step(
new_step_with_default_assertions("Hover back over the second pane's terminal")
.with_hover_on_saved_position_fn(|app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 1);
terminal_view.read(app, |terminal, _| terminal.terminal_position_id())
})
.add_assertion(assert_focused_pane_index(0, 1)),
)
.with_step(
new_step_with_default_assertions("Create another new session in a split pane")
.with_keystrokes(&[cmd_or_ctrl_shift("d")]),
)
.with_step(wait_until_bootstrapped_pane(0, 2))
.with_step(
new_step_with_default_assertions(
"Make sure the pane is focused even though the mouse is over the first pane",
)
.add_assertion(assert_focused_pane_index(0, 2)),
)
.with_step(
new_step_with_default_assertions("Disable focus pane on hover").add_assertion(
|app, _| {
PaneSettings::handle(app).update(app, |settings, ctx| {
settings
.focus_panes_on_hover
.set_value(false, ctx)
.expect("error updating setting");
async_assert!(!*settings.focus_panes_on_hover)
})
},
),
)
.with_step(
new_step_with_default_assertions(
"Hover over the initial pane's terminal and make sure it's not focused",
)
.with_hover_on_saved_position_fn(|app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
terminal_view.read(app, |terminal, _| terminal.terminal_position_id())
})
.add_assertion(assert_focused_pane_index(0, 2)),
)
}
pub fn test_close_tab_with_long_running_process() -> Builder {
new_builder()
.set_should_run_test(|| cfg!(target_os = "linux"))
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
new_step_with_default_assertions("Open a new tab")
.with_click_on_saved_position(NEW_TAB_BUTTON_POSITION_ID),
)
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
.with_step(
TestStep::new("Execute long-running command")
.with_typed_characters(&["python3"])
.with_keystrokes(&["enter"])
.add_assertion(
assert_long_running_block_executing_for_single_terminal_in_tab(true, 1),
),
)
.with_step(
new_step_with_default_assertions("Close the tab with a long-running command")
.with_hover_over_saved_position("close_tab_button:1")
.with_click_on_saved_position("close_tab_button:1")
.add_assertion(assert_tab_count(2))
.add_assertion(
// The tab should not yet be closed.
assert_long_running_block_executing_for_single_terminal_in_tab(true, 1),
),
)
// Press the confirm button in the modal.
.with_step(press_native_modal_button(0))
.with_step(TestStep::new("Wait for tab to close").add_assertion(assert_tab_count(1)))
}
+12
View File
@@ -0,0 +1,12 @@
use std::collections::HashMap;
use warp::settings::INPUT_MODE;
use warp::terminal::block_list_viewport::InputMode;
/// Returns a user defaults map with the `InputMode` set to `input_mode`.
#[allow(dead_code)]
pub fn input_mode(input_mode: InputMode) -> HashMap<String, String> {
HashMap::from_iter([(
INPUT_MODE.to_owned(),
serde_json::to_string(&input_mode).expect("input_mode value should convert to json string"),
)])
}
+238
View File
@@ -0,0 +1,238 @@
use itertools::Itertools as _;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::{
fs::{create_dir_all, write},
path::Path,
};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use version_compare::Version;
use crate::builder::cargo_target_tmpdir;
use warp::{
integration_testing::{
terminal::util::{
current_shell_starter_and_version, default_histfile_directory, ExpectedOutput,
},
view_getters,
},
terminal::shell::ShellType,
};
use warpui::{App, WindowId};
use warp::terminal::shell;
pub fn get_input_buffer(
app: &App,
window_id: WindowId,
tab_index: usize,
pane_index: usize,
) -> String {
view_getters::input_view(app, window_id, tab_index, pane_index)
.read(app, |input, app| input.buffer_text(app))
}
#[derive(EnumIter)]
pub enum ShellRcType {
Bash,
Zsh,
Fish,
PowerShell,
}
impl ShellRcType {
/// Returns the potential paths to the RC file relative to the `home` directory.
fn rc_file_paths(&self, home_dir: impl AsRef<Path>) -> Vec<PathBuf> {
let relative_paths = match self {
ShellRcType::Bash => vec![Path::new(".bash_profile")],
ShellRcType::Zsh => vec![Path::new(".zshrc")],
ShellRcType::Fish => vec![Path::new(".config/fish/config.fish")],
#[cfg(not(windows))]
ShellRcType::PowerShell => {
vec![Path::new(
".config/powershell/Microsoft.PowerShell_profile.ps1",
)]
}
// We need to make sure this works for either editor of PowerShell (PowerShell Core or
// Windows PowerShell) so just write the file to both.
#[cfg(windows)]
ShellRcType::PowerShell => vec![
Path::new("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
Path::new("Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1"),
],
};
relative_paths
.iter()
.map(|relative_path| home_dir.as_ref().join(relative_path))
.collect()
}
}
/// Sets the location of the ZSH `HISTFILE` to the home directory.
/// ZSH does not have a default location for the HISTFILE. However, MacOS has a custom `/etc/zshrc`
/// file that sets the default location of the `HISTFILE` to be located within the home directory.
/// To ensure we our tests are consistent across platforms, we set the value of `HISTFILE` to
/// `HOME` in the same way MacOS does.
pub fn set_zsh_histfile_location(dir: impl AsRef<Path>) {
let path = ShellRcType::Zsh
.rc_file_paths(dir)
.into_iter()
.exactly_one()
.expect("zsh only has one RC file path");
create_dir_all(path.parent().expect("Parent of RC file should exist"))
.expect("Should be able to create path to RC file");
let mut rc_file = OpenOptions::new()
.append(true)
.create(true)
.open(path)
.expect("Cannot open zshrc file");
rc_file
.write_all("\nHISTFILE=${ZDOTDIR:-$HOME}/.zsh_history".as_bytes())
.expect("Failed to write to zshrc file");
}
/// Usually, the title is the pwd where the home dir is shortened as "~".
/// However, this wasn't the case in Fish prior to version 3.4.0, see:
/// https://github.com/fish-shell/fish-shell/commit/698b8189356c8224443fdfc4399408f932d53aca
pub(crate) fn tab_title_in_home_dir(home_suffix: &str) -> String {
let (shell_starter, shell_version) = current_shell_starter_and_version();
let shell_version = Version::from(&shell_version).expect("shell version must be valid");
if shell_starter.shell_type() == ShellType::Fish
&& shell_version < Version::from("3.4.0").expect("shell version must be valid")
{
let home_path = Path::new(&cargo_target_tmpdir::get()).join(home_suffix);
format!(
"fish {}",
home_path.to_str().expect("path must be valid unicode")
)
} else {
String::from("~")
}
}
/// Writes the `rc_contents` into the corresponding RC files depending on the value of
/// `ShellRcType`.
pub fn write_rc_files_for_test<P, C>(
dir: P,
rc_contents: C,
shell_rc_types: impl IntoIterator<Item = ShellRcType>,
) where
P: AsRef<Path>,
C: AsRef<str>,
{
for rc_type in shell_rc_types.into_iter() {
let path_ref = dir.as_ref();
let paths = rc_type.rc_file_paths(path_ref);
for path in paths {
create_dir_all(path.parent().expect("Parent of RC file path should exist"))
.expect("Should be able to create path to RC file");
if let Err(e) = write(&path, rc_contents.as_ref()) {
panic!("Could not write rc file {:?}: {}", path.to_str(), e);
}
}
}
}
/// Writes the same `rc_contents` for all possible shell types supported by Warp.
pub fn write_all_rc_files_for_test<P, C>(dir: P, rc_contents: C)
where
P: AsRef<Path>,
C: AsRef<str>,
{
write_rc_files_for_test(dir, rc_contents, ShellRcType::iter())
}
/// Writes a histfile for `shell_types` to the given `dir`.
///
/// `commands` are written in the order that they're specified in the given vector; this means the
/// commands at the beginning of the vector read as if they were executed before commands at the end
/// of the vector.
///
/// Each histfile is written in the `ShellType`'s expected format.
pub fn write_histfiles_for_test<P>(
home_dir: P,
commands: Vec<&'static str>,
shell_types: impl IntoIterator<Item = ShellType>,
) where
P: AsRef<Path>,
{
for shell_type in shell_types.into_iter() {
let histfile_dir = default_histfile_directory(&shell_type, home_dir.as_ref());
let path_ref = histfile_dir.as_path();
create_dir_all(path_ref)
.expect("Should be able to create {shell_type:?} config directories");
let path = match shell_type {
ShellType::Bash => path_ref.join(".bash_history"),
ShellType::Zsh => path_ref.join(".zsh_history"),
ShellType::Fish => path_ref.join("fish_history"),
ShellType::PowerShell => path_ref.join("ConsoleHost_history.txt"),
};
let histfile_contents = match shell_type {
ShellType::Bash | ShellType::PowerShell => {
let mut contents = "".to_owned();
for command in commands.clone() {
contents += format!("{command}\n").as_str();
}
contents
}
ShellType::Fish => {
let mut contents = "".to_owned();
for command in commands.clone() {
println!("COMMAND:{command}");
contents += format!(
"- cmd: {}\n when: {}\n",
command,
chrono::Local::now().timestamp()
)
.as_str();
}
contents
}
ShellType::Zsh => {
let mut contents = "".to_owned();
for command in commands.clone() {
contents +=
format!(": {}:0;{}\n", chrono::Local::now().timestamp(), command).as_str();
}
contents
}
};
println!("histfile path:{:?}", &path);
if let Err(e) = write(&path, histfile_contents.as_bytes()) {
panic!("Could not write histfile {:?}: {}", path.to_str(), e);
}
}
}
/// Returns the string (ie. expected output etc) for the shell currently used for testing.
pub fn per_shell_output(
per_shell_output: Vec<(shell::ShellType, &str)>,
) -> impl ExpectedOutput + '_ {
let (starter, _) = current_shell_starter_and_version();
for (shell_type, output) in per_shell_output {
if starter.shell_type() == shell_type {
return Some(output);
}
}
None
}
/// Indicates a test that currently does not work in powershell. As part of CORE-2303, we should
/// eventually be removing all uses of this function.
pub fn skip_if_powershell_core_2303() -> bool {
let (starter, _) = current_shell_starter_and_version();
!matches!(starter.shell_type(), ShellType::PowerShell)
}
/// Gets the name of the system user for which the test binary is running.
pub fn get_local_user() -> String {
whoami::username()
}