From c8f66fc86ff29b51fee0eae1cb41a9df915efdcf Mon Sep 17 00:00:00 2001 From: Liam Bigelow <40188355+bglw@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:53:34 +1100 Subject: [PATCH] Debugger mode --- docs/content/docs/_index.md | 1 + docs/content/docs/configuration.md | 3 ++ docs/content/docs/debugger.md | 27 ++++++++++++++ toolproof/src/definitions/browser/mod.rs | 12 +++++-- toolproof/src/main.rs | 8 +++++ toolproof/src/options.rs | 14 ++++++++ toolproof/src/runner.rs | 45 ++++++++++++++++++++++++ 7 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 docs/content/docs/debugger.md diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 27578ba..ae30128 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -116,6 +116,7 @@ npx toolproof - [Syntax and Terminology](syntax/): Learn the full syntax for writing tests - [Browser Testing](browser-testing/): Comprehensive guide to testing web applications +- [Debugger Mode](debugger/): Step through tests interactively for debugging and development - [Using Macros](macros/): Create reusable step sequences - [Snapshot Testing](snapshots/): Snapshot test long or complex output - [Configuration](configuration/): Configure Toolproof for your project diff --git a/docs/content/docs/configuration.md b/docs/content/docs/configuration.md index 37918db..6d7f239 100644 --- a/docs/content/docs/configuration.md +++ b/docs/content/docs/configuration.md @@ -54,6 +54,7 @@ All configuration options that can be set via command-line or environment variab | `supported_versions` | String | Error if Toolproof version doesn't match this range | | `failure_screenshot_location` | String | Directory to save browser screenshots when tests fail | | `retry_count` | Number | Number of times to retry failed tests before marking as failed | +| `debugger` | Boolean | Run in debugger mode with step-by-step execution (requires single test) | ## Command Line Options @@ -98,6 +99,7 @@ npx toolproof -c 20 | `--browser ` | Specify which browser to use for tests (chrome or pagebrowse, default: chrome) | | `--retry-count ` | Number of times to retry failed tests before marking them as failed | | `--failure-screenshot-location ` | If set, Toolproof will screenshot the browser to this location when a test fails | +| `--debugger` | Run in debugger mode with step-by-step execution (requires single test with --name) | ## Environment Variables @@ -118,3 +120,4 @@ Most options can also be set using environment variables: | `TOOLPROOF_SUPPORTED_VERSIONS` | Error if Toolproof does not match this version range | | `TOOLPROOF_FAILURE_SCREENSHOT_LOCATION` | Location for browser screenshots on test failure | | `TOOLPROOF_RETRY_COUNT` | Number of times to retry failed tests | +| `TOOLPROOF_DEBUGGER` | Run in debugger mode with step-by-step execution | diff --git a/docs/content/docs/debugger.md b/docs/content/docs/debugger.md new file mode 100644 index 0000000..f434a37 --- /dev/null +++ b/docs/content/docs/debugger.md @@ -0,0 +1,27 @@ +--- +title: "Debugger Mode" +nav_title: "Debugger" +nav_section: Root +weight: 10 +--- + +Toolproof's debugger mode allows you to run tests step-by-step, making it easier to understand test behavior, debug failures, and develop new tests. + +## Enabling Debugger Mode + +Run Toolproof with the `--debugger` flag along with a specific test: + +```bash +npx toolproof --debugger --name "My Test Name" +``` + +Debugger mode requires running a single test. If you don't specify a test name, Toolproof will show an error. + +When running in debugger mode: + +- The browser runs with a visible window (not headless) +- Before each step executes, Toolproof pauses and shows you: + - The upcoming step to be executed + - The step's arguments (if any) + - The temporary directory path + - The server port (if a server is running) diff --git a/toolproof/src/definitions/browser/mod.rs b/toolproof/src/definitions/browser/mod.rs index f6d9310..7547dba 100644 --- a/toolproof/src/definitions/browser/mod.rs +++ b/toolproof/src/definitions/browser/mod.rs @@ -63,13 +63,18 @@ pub enum BrowserTester { }, } -async fn try_launch_browser(mut max: usize) -> (Browser, chromiumoxide::Handler) { +async fn try_launch_browser(mut max: usize, visible: bool) -> (Browser, chromiumoxide::Handler) { let mut launch = Err(CdpError::NotFound); while launch.is_err() && max > 0 { max -= 1; + let headless_mode = if visible { + chromiumoxide::browser::HeadlessMode::False + } else { + chromiumoxide::browser::HeadlessMode::New + }; launch = Browser::launch( BrowserConfig::builder() - .headless_mode(chromiumoxide::browser::HeadlessMode::New) + .headless_mode(headless_mode) .user_data_dir(tempdir().expect("testing on a system with a temp dir")) .viewport(Some(Viewport { width: 1600, @@ -101,7 +106,8 @@ impl BrowserTester { async fn initialize(params: &ToolproofParams) -> Self { match params.browser { crate::options::ToolproofBrowserImpl::Chrome => { - let (browser, mut handler) = try_launch_browser(3).await; + let visible = params.debugger; + let (browser, mut handler) = try_launch_browser(3, visible).await; BrowserTester::Chrome { browser: Arc::new(browser), diff --git a/toolproof/src/main.rs b/toolproof/src/main.rs index 84d37f6..5ea19ce 100644 --- a/toolproof/src/main.rs +++ b/toolproof/src/main.rs @@ -401,6 +401,14 @@ async fn main_inner() -> Result<(), ()> { RunMode::All }; + // Debugger mode requires running a single test + if universe.ctx.params.debugger && !matches!(run_mode, RunMode::One(_)) { + eprintln!( + "Debugger mode requires running a single test. Please specify a test using --name." + ); + return Err(()); + } + enum HoldingError { TestFailure, SnapFailure { out: String }, diff --git a/toolproof/src/options.rs b/toolproof/src/options.rs index ad00254..aab2986 100644 --- a/toolproof/src/options.rs +++ b/toolproof/src/options.rs @@ -152,6 +152,12 @@ fn get_cli_matches() -> ArgMatches { .required(false) .value_parser(value_parser!(PathBuf)), ) + .arg( + arg!( + --debugger ... "Run in debugger mode with step-by-step execution" + ) + .action(clap::ArgAction::SetTrue), + ) .get_matches() } @@ -240,6 +246,10 @@ pub struct ToolproofParams { #[setting(env = "TOOLPROOF_RETRY_COUNT")] #[setting(default = 0)] pub retry_count: usize, + + /// Run in debugger mode with step-by-step execution + #[setting(env = "TOOLPROOF_DEBUGGER")] + pub debugger: bool, } // The configuration object used internally @@ -333,5 +343,9 @@ impl ToolproofParams { if let Some(retry_count) = cli_matches.get_one::("retry-count") { self.retry_count = *retry_count; } + + if cli_matches.get_flag("debugger") { + self.debugger = true; + } } } diff --git a/toolproof/src/runner.rs b/toolproof/src/runner.rs index 8a262d5..f0c3049 100644 --- a/toolproof/src/runner.rs +++ b/toolproof/src/runner.rs @@ -1,6 +1,8 @@ use async_recursion::async_recursion; +use console::Term; use normalize_path::NormalizePath; +use schematic::color::owo::OwoColorize; use similar_string::find_best_similarity; use std::{ collections::HashMap, @@ -78,6 +80,37 @@ pub async fn run_toolproof_experiment( res } +fn debugger_pause(step: &ToolproofTestStep, civ: &Civilization) { + if !civ.universe.ctx.params.debugger { + return; + } + + let term = Term::stdout(); + + println!("\n{}", "--- DEBUGGER ---".on_blue().bold()); + + if let Some(ref tmp_dir) = civ.tmp_dir { + println!("Temp directory: {}", tmp_dir.path().to_string_lossy()); + } + + if let Some(port) = civ.assigned_server_port { + println!("Server hosted at: http://localhost:{}", port); + } + + println!("\nNext:"); + println!(" - step: {}", step); + let args_pretty = step.args_pretty(); + if !args_pretty.trim().is_empty() { + for line in args_pretty.lines() { + println!(" {}", line); + } + } + + println!("\n{}", "Press [Enter] to continue...".dimmed()); + + let _ = term.read_line(); +} + #[async_recursion] async fn run_toolproof_steps( file_directory: &String, @@ -119,6 +152,8 @@ async fn run_toolproof_steps( state, platforms, } => { + debugger_pause(&marked_base_step, civ); + let target_path = PathBuf::from(file_directory) .join(other_file) .normalize() @@ -167,6 +202,8 @@ async fn run_toolproof_steps( state, platforms, } => { + debugger_pause(&marked_base_step, civ); + let Some((reference_segments, defined_macro)) = civ.universe.macros.get_key_value(step_macro) else { @@ -229,6 +266,8 @@ async fn run_toolproof_steps( platforms, .. } => { + debugger_pause(&marked_base_step, civ); + let Some((reference_segments, instruction)) = civ.universe.instructions.get_key_value(step) else { @@ -274,6 +313,8 @@ async fn run_toolproof_steps( platforms, .. } => { + debugger_pause(&marked_base_step, civ); + let Some((reference_ret, retrieval_step)) = civ.universe.retrievers.get_key_value(retrieval) else { @@ -354,6 +395,8 @@ async fn run_toolproof_steps( state, platforms, } => { + debugger_pause(&marked_base_step, civ); + let Some((reference_ret, retrieval_step)) = civ.universe.retrievers.get_key_value(snapshot) else { @@ -405,6 +448,8 @@ async fn run_toolproof_steps( state, platforms, } => { + debugger_pause(&marked_base_step, civ); + let Some((reference_ret, retrieval_step)) = civ.universe.retrievers.get_key_value(extract) else {