//! Integration tests for `help` and `help ` (H3). //! //! Covers: //! - Parse layer: `help` → `Help { topic: None }`; `help insert` //! → `Help { topic: Some("insert") }`. //! - App behaviour: the full `help` ends with the detail hint; //! `help ` renders that command's block (and every //! form sharing the entry word); `help types` renders the type //! reference; an unknown topic gets a friendly pointer back. use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rdbms_playground::app::App; use rdbms_playground::dsl::{AppCommand, Command, parse_command}; use rdbms_playground::event::AppEvent; const fn key(code: KeyCode) -> AppEvent { AppEvent::Key(KeyEvent { code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: crossterm::event::KeyEventState::NONE, }) } fn type_str(app: &mut App, s: &str) { for c in s.chars() { app.update(key(KeyCode::Char(c))); } } /// Submit `input` to a fresh app and collect all output text. fn output_for(input: &str) -> Vec { let mut app = App::new(); type_str(&mut app, input); app.update(key(KeyCode::Enter)); app.output.iter().map(|l| l.text.clone()).collect() } // ================================================================= // Parse layer // ================================================================= #[test] fn bare_help_parses_with_no_topic() { assert_eq!( parse_command("help").expect("parses"), Command::App(AppCommand::Help { topic: None }), ); } #[test] fn help_with_topic_captures_the_word() { assert_eq!( parse_command("help insert").expect("parses"), Command::App(AppCommand::Help { topic: Some("insert".to_string()) }), ); } #[test] fn help_topic_is_a_single_word_multi_word_is_a_parse_error() { // Entry-word topics cover multi-word commands (`help create`), // so a second word is trailing junk, not a longer topic. assert!(parse_command("help foo bar").is_err()); } // ================================================================= // App behaviour // ================================================================= #[test] fn full_help_lists_commands_and_ends_with_the_detail_hint() { let out = output_for("help"); assert!( out.iter().any(|l| l == "Supported commands:"), "intro present: {out:?}", ); assert!( out.iter().any(|l| l.contains("help ")), "detail-hint footer present: {out:?}", ); } #[test] fn help_insert_renders_the_insert_block() { let out = output_for("help insert"); assert!( out.iter().any(|l| l.contains("insert into")), "insert help shown: {out:?}", ); // Focused: it must NOT dump the whole list — the intro header // belongs to the full `help` only. assert!( !out.iter().any(|l| l == "Supported commands:"), "focused help omits the full-list intro: {out:?}", ); } #[test] fn help_create_covers_every_form_sharing_the_entry_word() { // `create` is the entry word for both the DSL `create table` // and the advanced SQL `CREATE TABLE` — `help create` shows // both blocks. let out = output_for("help create"); let joined = out.join("\n"); assert!( joined.contains("create table"), "DSL create form shown: {out:?}", ); assert!( joined.to_lowercase().matches("create").count() >= 2, "more than one create form shown: {out:?}", ); } // ----- issue #36: advanced-mode SQL forms get distinct help content ----- #[test] fn help_select_renders_the_sql_select_block() { // `select` is advanced-only (no simple sibling) and used to have // `help_id: None`, so `help select` produced the unknown-topic note. // It now carries its own help page. let out = output_for("help select"); let joined = out.join("\n").to_lowercase(); assert!( joined.contains("select") && joined.contains("from"), "help select shows the SQL select form: {out:?}", ); assert!( !out.iter().any(|l| l.contains("No help for")), "help select resolves to content, not the unknown-topic note: {out:?}", ); } #[test] fn help_with_renders_the_cte_block() { let out = output_for("help with"); let joined = out.join("\n").to_lowercase(); assert!( joined.contains("with") && joined.contains("as ("), "help with shows the CTE form: {out:?}", ); assert!( !out.iter().any(|l| l.contains("No help for")), "help with resolves to content: {out:?}", ); } #[test] fn help_insert_shows_both_simple_and_sql_forms() { // `help insert` now covers the simple form AND the advanced SQL form // (two clearly-labelled blocks, like `help create` already does). let out = output_for("help insert"); let joined = out.join("\n").to_lowercase(); assert!( joined.contains("insert into"), "simple insert form shown: {out:?}", ); assert!( joined.contains("advanced"), "advanced SQL insert form shown alongside the simple one: {out:?}", ); } #[test] fn help_list_splits_simple_and_advanced_sections() { let out = output_for("help"); let joined = out.join("\n"); assert!( out.iter().any(|l| l.contains("Simple-mode commands")), "simple-mode section header present: {out:?}", ); assert!( out.iter() .any(|l| l.contains("Advanced-mode") && l.contains("SQL")), "advanced-mode (SQL) section header present: {out:?}", ); // Copy rule: never say "DSL" in user-facing text (the old header did). assert!( !joined.contains("DSL"), "help output must not contain 'DSL': {out:?}", ); // The advanced query commands are now discoverable in the list. assert!( joined.to_lowercase().contains("select"), "select is listed in help: {out:?}", ); } #[test] fn help_types_renders_the_type_reference() { let out = output_for("help types"); let joined = out.join("\n").to_lowercase(); // The type reference names the playground types. assert!( joined.contains("serial") || joined.contains("shortid"), "type reference shown: {out:?}", ); } #[test] fn help_unknown_topic_points_back_to_the_full_list() { let out = output_for("help wibble"); assert!( out.iter() .any(|l| l.contains("No help for") && l.contains("wibble")), "names the unknown topic: {out:?}", ); assert!( out.iter().any(|l| l.contains("Type `help`")), "points back at the full list: {out:?}", ); }