| 1 | //! Search owns printable keys from the first character, through the real |
| 2 | //! terminal decoder and modal host. No provider or saved user config is used. |
| 3 | |
| 4 | use std::time::Duration; |
| 5 | |
| 6 | use super::qa_harness::{ |
| 7 | harness::{Harness, make_sealed_workspace}, |
| 8 | keys, |
| 9 | }; |
| 10 | |
| 11 | #[test] |
| 12 | fn search_text_stays_in_modal_and_out_of_composer() { |
| 13 | let workspace = make_sealed_workspace().expect("sealed workspace"); |
| 14 | let mut tui = Harness::builder(Harness::cargo_bin("codewhale-tui")) |
| 15 | .cwd(workspace.workspace()) |
| 16 | .clear_env() |
| 17 | .seal_home(workspace.home()) |
| 18 | .env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1") |
| 19 | .env("CODEWHALE_NO_UPDATE_CHECK", "1") |
| 20 | .env("CODEWHALE_TELEMETRY", "0") |
| 21 | .env("NO_ANIMATIONS", "1") |
| 22 | .args([ |
| 23 | "--workspace", |
| 24 | workspace.workspace().to_str().unwrap(), |
| 25 | "--no-project-config", |
| 26 | "--fresh", |
| 27 | ]) |
| 28 | .size(24, 80) |
| 29 | .spawn() |
| 30 | .expect("start TUI"); |
| 31 | let timeout = Duration::from_secs(10); |
| 32 | tui.wait_for_text("Type a message", timeout).unwrap(); |
| 33 | // Enter the live shell with a local command, without making a model call. |
| 34 | tui.paste("/help").unwrap(); |
| 35 | tui.wait_for_text("❯ /help", timeout).unwrap(); |
| 36 | tui.send(keys::key::enter()).unwrap(); |
| 37 | tui.wait_for_text("Help —", timeout).unwrap(); |
| 38 | tui.send(keys::key::esc()).unwrap(); |
| 39 | tui.wait_for_text("Type a message", timeout).unwrap(); |
| 40 | let draft = "draft_no_leak"; |
| 41 | tui.paste(draft).unwrap(); |
| 42 | tui.wait_for_text(draft, timeout).unwrap(); |
| 43 | |
| 44 | for (open, title, prefix, query, escapes) in [ |
| 45 | (keys::key::f1(), "Help —", "Filter: ", "queue", 1), |
| 46 | (keys::key::f1(), "Help —", "Filter: ", "Queue", 1), |
| 47 | (keys::key::f2(), "Config", "Search: ", "quiet", 2), |
| 48 | (keys::key::f2(), "Config", "Search: ", "effort", 2), |
| 49 | (keys::key::f2(), "Config", "Search: ", "json", 2), |
| 50 | (keys::key::f2(), "Config", "Search: ", "key", 2), |
| 51 | (keys::key::f2(), "Config", "Search: ", " 队列é", 2), |
| 52 | (keys::key::ctrl('k'), "Command —", "Filter: ", "json", 1), |
| 53 | (keys::key::ctrl('k'), "Command —", "Filter: ", "key", 1), |
| 54 | ] { |
| 55 | tui.send(open).unwrap(); |
| 56 | tui.wait_for_text(title, timeout).unwrap(); |
| 57 | let mut typed = String::new(); |
| 58 | for ch in query.chars() { |
| 59 | tui.send(ch.to_string()).unwrap(); |
| 60 | typed.push(ch); |
| 61 | tui.wait_for_text(&format!("{prefix}{typed}"), timeout) |
| 62 | .unwrap_or_else(|error| panic!("{title} query {typed:?}: {error}")); |
| 63 | } |
| 64 | for _ in 0..escapes { |
| 65 | tui.send(keys::key::esc()).unwrap(); |
| 66 | tui.wait_for_idle(Duration::from_millis(150), timeout) |
| 67 | .unwrap(); |
| 68 | } |
| 69 | tui.wait_for_text(draft, timeout).unwrap(); |
| 70 | let frame = tui.frame(); |
| 71 | let composer = (0..frame.rows()) |
| 72 | .map(|row| frame.row(row)) |
| 73 | .find(|row| row.contains('❯')) |
| 74 | .expect("composer is visible after closing search"); |
| 75 | let content = composer |
| 76 | .split_once('❯') |
| 77 | .unwrap() |
| 78 | .1 |
| 79 | .split("[↵]") |
| 80 | .next() |
| 81 | .unwrap() |
| 82 | .trim_end(); |
| 83 | let content = content.strip_suffix('│').unwrap_or(content).trim(); |
| 84 | assert_eq!(content, draft, "{title} leaked query {query:?}"); |
| 85 | } |
| 86 | tui.shutdown(); |
| 87 | } |
| 88 |