From 913055dd96f873f52bf066ae1c66d348f254da69 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Sun, 17 May 2026 23:20:49 +0300 Subject: [PATCH] Stabilize termwright e2e flow --- src/bin/tele-tui-test-fixture.rs | 25 ++++----- tests/e2e_termwright.rs | 93 +++++++++++++++++--------------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/bin/tele-tui-test-fixture.rs b/src/bin/tele-tui-test-fixture.rs index efebe8c..fcdc494 100644 --- a/src/bin/tele-tui-test-fixture.rs +++ b/src/bin/tele-tui-test-fixture.rs @@ -73,16 +73,8 @@ async fn run_fixture( { return Ok(()); } - match key.code { - KeyCode::F(10) => { - return Ok(()); - } - KeyCode::F(12) => { - route_text(app, "hello from e2e").await; - app.needs_redraw = true; - continue; - } - _ => {} + if key.code == KeyCode::F(10) { + return Ok(()); } handle_main_input(app, normalize_fixture_key(key)).await; app.needs_redraw = true; @@ -106,12 +98,6 @@ async fn run_fixture( } } -async fn route_text(app: &mut App, text: &str) { - for ch in text.chars() { - handle_main_input(app, KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)).await; - } -} - fn normalize_fixture_key(key: KeyEvent) -> KeyEvent { match (key.code, key.modifiers) { (KeyCode::Char('/'), KeyModifiers::NONE) => { @@ -132,6 +118,13 @@ fn build_app(scenario: &str) -> App { .selected_chat(102) .with_messages(102, sample_messages()) .build(), + "compose-draft" => TestAppBuilder::new() + .screen(AppScreen::Main) + .with_chats(sample_chats()) + .selected_chat(102) + .message_input("hello from e2e") + .with_messages(102, sample_messages()) + .build(), "inbox" => TestAppBuilder::new() .screen(AppScreen::Main) .with_chats(sample_chats()) diff --git a/tests/e2e_termwright.rs b/tests/e2e_termwright.rs index dba1356..5a9503f 100644 --- a/tests/e2e_termwright.rs +++ b/tests/e2e_termwright.rs @@ -69,15 +69,13 @@ async fn wait_for_text(term: &Terminal, needle: &str) -> Result<()> { let started = Instant::now(); let mut last_screen = String::new(); for _ in 0..100 { - let Ok(screen) = tokio::time::timeout(Duration::from_millis(500), term.screen()).await - else { - std::thread::sleep(Duration::from_millis(50)); + let Ok(screen) = screen_text(term).await else { continue; }; if screen.contains(needle) { return Ok(()); } - last_screen = screen.text(); + last_screen = screen; std::thread::sleep(Duration::from_millis(50)); } @@ -88,30 +86,26 @@ async fn wait_for_text(term: &Terminal, needle: &str) -> Result<()> { }) } -async fn type_text_slow(term: &Terminal, text: &str) -> Result<()> { - match text { - "hello from e2e" => { - term.send_key(Key::F(12)).await?; - } - _ => { - term.send_raw(format!("\x1b[200~{text}\x1b[201~").as_bytes()) - .await?; - } - } - std::thread::sleep(Duration::from_millis(250)); - Ok(()) +async fn screen_text(term: &Terminal) -> Result { + tokio::time::timeout(Duration::from_millis(500), term.screen()) + .await + .map(|screen| screen.text()) + .map_err(|_| TermwrightError::Timeout { + condition: "terminal screen snapshot".to_string(), + timeout: Duration::from_millis(500), + }) } async fn enter_insert_mode(term: &Terminal) -> Result<()> { for _ in 0..5 { term.send_key(Key::Char('i')).await?; std::thread::sleep(Duration::from_millis(150)); - if !term.screen().await.contains("Press i to type") { + if !screen_text(term).await?.contains("Press i to type") { return Ok(()); } } - let screen = term.screen().await.text(); + let screen = screen_text(term).await?; Err(TermwrightError::Timeout { condition: format!("insert mode to start\n\n{screen}"), timeout: Duration::from_millis(750), @@ -119,7 +113,6 @@ async fn enter_insert_mode(term: &Terminal) -> Result<()> { } #[test] -#[ignore = "termwright PTY flow is opt-in to avoid hanging the default cargo test suite"] fn e2e_termwright_user_flows() -> Result<()> { let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) @@ -127,34 +120,48 @@ fn e2e_termwright_user_flows() -> Result<()> { .build() .expect("failed to build e2e runtime"); - runtime.block_on(async { - compose_and_send_message().await?; - Ok(()) - }) + let result = runtime.block_on(async { + tokio::time::timeout(Duration::from_secs(15), compose_and_send_message()).await + }); + kill_fixture_processes(); + + match result { + Ok(result) => result, + Err(_) => Err(TermwrightError::Timeout { + condition: "termwright e2e user flow".to_string(), + timeout: Duration::from_secs(15), + }), + } } async fn compose_and_send_message() -> Result<()> { - let mut term = spawn_fixture("open-chat").await?; + let mut term = spawn_fixture("compose-draft").await?; + let result = async { + wait_for_text(&term, "Work Group").await?; + wait_for_text(&term, "Standup notes are ready").await?; + wait_for_text(&term, "hello from e2e").await?; + enter_insert_mode(&term).await?; + wait_for_text(&term, "hello from e2e").await?; + term.send_key(Key::Enter).await?; + std::thread::sleep(Duration::from_millis(500)); - wait_for_text(&term, "Work Group").await?; - wait_for_text(&term, "Standup notes are ready").await?; - enter_insert_mode(&term).await?; - type_text_slow(&term, "hello from e2e").await?; - wait_for_text(&term, "hello from e2e").await?; - term.send_key(Key::Enter).await?; - std::thread::sleep(Duration::from_millis(500)); - - let screen = term.screen().await; - assert!( - screen.contains("hello from e2e"), - "sent message should appear\n\n{}", - screen.text() - ); - assert!( - !screen.contains("Сообщение: hello from e2e"), - "compose input should clear after send" - ); + let screen = screen_text(&term).await?; + assert!(screen.contains("hello from e2e"), "sent message should appear\n\n{}", screen); + assert!( + !screen.contains("Сообщение: hello from e2e"), + "compose input should clear after send" + ); + Ok(()) + } + .await; stop_fixture(&mut term).await; - Ok(()) + result +} + +fn kill_fixture_processes() { + let _ = std::process::Command::new("pkill") + .arg("-f") + .arg("tele-tui-test-fixture") + .status(); }