Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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(®ex, 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"))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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"),
|
||||
)])
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user