Initial public release of Warp.

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