//! 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, ) }) }), ) }