From ff77724e2b2cd7b6f3f2ac6f8399a6867848a39d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Nov 2025 19:55:49 +0000 Subject: [PATCH] Fix contact add link to use Contacts_Edit_Action SPA route Add multi-route support for controllers and SPA actions Add screenshot feature to rsx:debug and convert contacts edit to SPA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Commands/Rsx/Route_Debug_Command.php | 33 ++- .../Core/Dispatch/Route_ManifestSupport.php | 20 +- app/RSpade/Core/Js/Rsx.js | 73 +++++- app/RSpade/Core/Rsx.php | 130 +++++---- app/RSpade/Core/SPA/Spa_Layout.js | 3 + app/RSpade/Core/SPA/Spa_ManifestSupport.php | 19 +- app/RSpade/man/routing.txt | 18 ++ app/RSpade/man/rsx_debug.txt | 247 ++++++++++++++++++ app/RSpade/man/spa.txt | 6 + bin/route-debug.js | 64 ++++- docs/CLAUDE.dist.md | 3 + 11 files changed, 555 insertions(+), 61 deletions(-) create mode 100755 app/RSpade/man/rsx_debug.txt diff --git a/app/RSpade/Commands/Rsx/Route_Debug_Command.php b/app/RSpade/Commands/Rsx/Route_Debug_Command.php index 801c8b189..f66863e63 100755 --- a/app/RSpade/Commands/Rsx/Route_Debug_Command.php +++ b/app/RSpade/Commands/Rsx/Route_Debug_Command.php @@ -151,7 +151,9 @@ class Route_Debug_Command extends Command {--console-debug-benchmark : Include benchmark timing prefixes in console_debug output} {--console-debug-all : Show all console_debug channels (overrides filter)} {--console-debug-disable : Disable console_debug entirely for this test} - {--console-list : Alias for --console-log to display all console output}'; + {--console-list : Alias for --console-log to display all console output} + {--screenshot-width= : Screenshot width (px or preset: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large). Defaults to 1920} + {--screenshot-path= : Path to save screenshot file (triggers screenshot capture, max height 5000px)}'; /** * The console command description. @@ -263,6 +265,10 @@ class Route_Debug_Command extends Command $timeout = 30000; // Default 30 seconds } + // Get screenshot options + $screenshot_width = $this->option('screenshot-width'); + $screenshot_path = $this->option('screenshot-path'); + // Get console debug options (with environment variable fallbacks) $console_debug_filter = $this->option('console-debug-filter') ?: env('CONSOLE_DEBUG_FILTER'); $console_debug_benchmark = $this->option('console-debug-benchmark') ?: env('CONSOLE_DEBUG_BENCHMARK', false); @@ -458,6 +464,14 @@ class Route_Debug_Command extends Command $command_args[] = "--console-debug-disable"; } + if ($screenshot_width) { + $command_args[] = "--screenshot-width={$screenshot_width}"; + } + + if ($screenshot_path) { + $command_args[] = "--screenshot-path={$screenshot_path}"; + } + // Pass Laravel log path as environment variable $laravel_log_path = storage_path('logs/laravel.log'); @@ -526,8 +540,12 @@ class Route_Debug_Command extends Command $this->comment('DEBUGGING OUTPUT:'); $this->line(' php artisan rsx:debug / --console-log # All console output'); + $this->line(' php artisan rsx:debug / --console-list # Alias for --console-log'); $this->line(' php artisan rsx:debug / --console-debug-filter=AUTH # Filter console_debug'); + $this->line(' php artisan rsx:debug / --console-debug-all # Show all console_debug channels'); $this->line(' php artisan rsx:debug / --console-debug-benchmark # With timing'); + $this->line(' php artisan rsx:debug / --console-debug-disable # Disable console_debug'); + $this->line(' php artisan rsx:debug / --log # Display Laravel error log'); $this->line(' php artisan rsx:debug / --all-logs # Show all log files'); $this->line(''); @@ -555,6 +573,19 @@ class Route_Debug_Command extends Command $this->line(' php artisan rsx:debug /slow --timeout=60000 # 60 second timeout'); $this->line(''); + $this->comment('SCREENSHOTS:'); + $this->line(' php artisan rsx:debug /page --screenshot-path=/tmp/screenshot.png'); + $this->line(' # Screenshot at 1920px (default)'); + $this->line(' php artisan rsx:debug /page --screenshot-width=mobile --screenshot-path=/tmp/mobile.png'); + $this->line(' # Mobile device (412px)'); + $this->line(' php artisan rsx:debug /page --screenshot-width=tablet --screenshot-path=/tmp/tablet.png'); + $this->line(' # Tablet device (768px)'); + $this->line(' php artisan rsx:debug /page --screenshot-width=1024 --screenshot-path=/tmp/custom.png'); + $this->line(' # Custom width (1024px)'); + $this->line(' # Available presets: mobile (412px), iphone-mobile (390px), tablet (768px),'); + $this->line(' # desktop-small (1366px), desktop-medium (1920px), desktop-large (2560px)'); + $this->line(''); + $this->comment('IMPORTANT NOTES:'); $this->line(' • When using rsx:debug with grep and no output appears, re-run without grep'); $this->line(' to see the full context and any errors that may have occurred'); diff --git a/app/RSpade/Core/Dispatch/Route_ManifestSupport.php b/app/RSpade/Core/Dispatch/Route_ManifestSupport.php index d68bbd78e..565499f0a 100755 --- a/app/RSpade/Core/Dispatch/Route_ManifestSupport.php +++ b/app/RSpade/Core/Dispatch/Route_ManifestSupport.php @@ -28,10 +28,13 @@ class Route_ManifestSupport extends ManifestSupport_Abstract */ public static function process(array &$manifest_data): void { - // Initialize routes key + // Initialize routes structures if (!isset($manifest_data['data']['routes'])) { $manifest_data['data']['routes'] = []; } + if (!isset($manifest_data['data']['routes_by_target'])) { + $manifest_data['data']['routes_by_target'] = []; + } // Look for Route attributes - must check all namespaces since Route is not a real class // PHP attributes without an import will use the current namespace @@ -99,8 +102,8 @@ class Route_ManifestSupport extends ManifestSupport_Abstract ); } - // Store route with flat structure - $manifest_data['data']['routes'][$pattern] = [ + // Store route with flat structure (for dispatcher) + $route_data = [ 'methods' => array_map('strtoupper', (array) $methods), 'type' => $type, 'class' => $item['fqcn'] ?? $item['class'], @@ -108,7 +111,17 @@ class Route_ManifestSupport extends ManifestSupport_Abstract 'name' => $name, 'file' => $item['file'], 'require' => $require_attrs, + 'pattern' => $pattern, ]; + + $manifest_data['data']['routes'][$pattern] = $route_data; + + // Also store by target for URL generation (group multiple routes per controller method) + $target = $item['class'] . '::' . $item['method']; + if (!isset($manifest_data['data']['routes_by_target'][$target])) { + $manifest_data['data']['routes_by_target'][$target] = []; + } + $manifest_data['data']['routes_by_target'][$target][] = $route_data; } } } @@ -116,5 +129,6 @@ class Route_ManifestSupport extends ManifestSupport_Abstract // Sort routes alphabetically by path to ensure deterministic behavior and prevent race condition bugs ksort($manifest_data['data']['routes']); + ksort($manifest_data['data']['routes_by_target']); } } diff --git a/app/RSpade/Core/Js/Rsx.js b/app/RSpade/Core/Js/Rsx.js index abc93024c..7f52f87a5 100755 --- a/app/RSpade/Core/Js/Rsx.js +++ b/app/RSpade/Core/Js/Rsx.js @@ -248,7 +248,7 @@ class Rsx { pattern = Rsx._routes[class_name][action_name]; } else { // Not found in PHP routes - check if it's a SPA action - pattern = Rsx._try_spa_action_route(class_name); + pattern = Rsx._try_spa_action_route(class_name, params_obj); if (!pattern) { // Route not found - use default pattern /_/{controller}/{action} @@ -261,6 +261,60 @@ class Rsx { return Rsx._generate_url_from_pattern(pattern, params_obj); } + /** + * Select the best matching route pattern from available patterns based on provided parameters + * + * Selection algorithm: + * 1. Filter patterns where all required parameters can be satisfied by provided params + * 2. Among satisfiable patterns, prioritize those with MORE parameters (more specific) + * 3. If tie, any pattern works (deterministic by using first match) + * + * @param {Array} patterns Array of route patterns + * @param {Object} params_obj Provided parameters + * @returns {string|null} Selected pattern or null if none match + */ + static _select_best_route_pattern(patterns, params_obj) { + const satisfiable = []; + + for (const pattern of patterns) { + // Extract required parameters from pattern + const required_params = []; + const matches = pattern.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g); + if (matches) { + // Remove the : prefix from each match + for (const match of matches) { + required_params.push(match.substring(1)); + } + } + + // Check if all required parameters are provided + let can_satisfy = true; + for (const required of required_params) { + if (!(required in params_obj)) { + can_satisfy = false; + break; + } + } + + if (can_satisfy) { + satisfiable.push({ + pattern: pattern, + param_count: required_params.length + }); + } + } + + if (satisfiable.length === 0) { + return null; + } + + // Sort by parameter count descending (most parameters first) + satisfiable.sort((a, b) => b.param_count - a.param_count); + + // Return the pattern with the most parameters + return satisfiable[0].pattern; + } + /** * Generate URL from route pattern by replacing parameters * @@ -327,9 +381,10 @@ class Rsx { * Returns the route pattern or null if not found * * @param {string} class_name The action class name + * @param {Object} params_obj The parameters for route selection * @returns {string|null} The route pattern or null */ - static _try_spa_action_route(class_name) { + static _try_spa_action_route(class_name, params_obj) { // Get all classes from manifest const all_classes = Manifest.get_all_classes(); @@ -346,8 +401,18 @@ class Rsx { const routes = class_object._spa_routes || []; if (routes.length > 0) { - // Return the first route pattern - return routes[0]; + // Select best matching route based on parameters + const selected = Rsx._select_best_route_pattern(routes, params_obj); + + if (!selected) { + // Routes exist but none are satisfiable + throw new Error( + `No suitable route found for SPA action ${class_name} with provided parameters. ` + + `Available routes: ${routes.join(', ')}` + ); + } + + return selected; } } diff --git a/app/RSpade/Core/Rsx.php b/app/RSpade/Core/Rsx.php index 3e755ada1..f98d605a8 100755 --- a/app/RSpade/Core/Rsx.php +++ b/app/RSpade/Core/Rsx.php @@ -260,22 +260,11 @@ class Rsx shouldnt_happen("Method {$class_name}::{$action_name} in public_static_methods is not static - extraction bug"); } - // Check for Route or Ajax_Endpoint attribute - $has_route = false; + // Check for Ajax_Endpoint attribute $has_ajax_endpoint = false; - $route_pattern = null; if (isset($method_info['attributes'])) { foreach ($method_info['attributes'] as $attr_name => $attr_instances) { - if ($attr_name === 'Route' || str_ends_with($attr_name, '\\Route')) { - $has_route = true; - // Get the route pattern from the first instance - if (!empty($attr_instances)) { - $route_args = $attr_instances[0]; - $route_pattern = $route_args[0] ?? ($route_args['pattern'] ?? null); - } - break; - } if ($attr_name === 'Ajax_Endpoint' || str_ends_with($attr_name, '\\Ajax_Endpoint')) { $has_ajax_endpoint = true; break; @@ -293,17 +282,29 @@ class Rsx return $ajax_url; } - if (!$has_route) { - // Not a controller method with Route/Ajax - check if it's a SPA action class + // Look up routes in manifest using routes_by_target + $target = $class_name . '::' . $action_name; + $manifest = Manifest::get_full_manifest(); + + if (!isset($manifest['data']['routes_by_target'][$target])) { + // Not a controller method with Route - check if it's a SPA action class return static::_try_spa_action_route($class_name, $params_array); } - if (!$route_pattern) { - throw new Rsx_Caller_Exception("Route attribute on {$class_name}::{$action_name} must have a pattern"); + $routes = $manifest['data']['routes_by_target'][$target]; + + // Select best matching route based on provided parameters + $selected_route = static::_select_best_route($routes, $params_array); + + if (!$selected_route) { + throw new Rsx_Caller_Exception( + "No suitable route found for {$class_name}::{$action_name} with provided parameters. " . + "Available routes: " . implode(', ', array_column($routes, 'pattern')) + ); } - // Generate URL from pattern - return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, $action_name); + // Generate URL from selected pattern + return static::_generate_url_from_pattern($selected_route['pattern'], $params_array, $class_name, $action_name); } /** @@ -329,43 +330,82 @@ class Rsx throw new Rsx_Caller_Exception("JavaScript class {$class_name} must extend Spa_Action to generate routes"); } - // Get the file path for this JS class - try { - $file_path = Manifest::js_find_class($class_name); - } catch (\RuntimeException $e) { - throw new Rsx_Caller_Exception("SPA action class {$class_name} not found in manifest"); + // Look up routes in manifest using routes_by_target + $manifest = Manifest::get_full_manifest(); + + if (!isset($manifest['data']['routes_by_target'][$class_name])) { + throw new Rsx_Caller_Exception("SPA action {$class_name} has no registered routes in manifest"); } - // Get file metadata which contains decorator information - try { - $file_data = Manifest::get_file($file_path); - } catch (\RuntimeException $e) { - throw new Rsx_Caller_Exception("File metadata not found for SPA action {$class_name}"); + $routes = $manifest['data']['routes_by_target'][$class_name]; + + // Select best matching route based on provided parameters + $selected_route = static::_select_best_route($routes, $params_array); + + if (!$selected_route) { + throw new Rsx_Caller_Exception( + "No suitable route found for SPA action {$class_name} with provided parameters. " . + "Available routes: " . implode(', ', array_column($routes, 'pattern')) + ); } - // Extract route pattern from decorators - // JavaScript files have 'decorators' array in their metadata - // Format: [[0 => 'route', 1 => ['/contacts']], ...] - $route_pattern = null; - if (isset($file_data['decorators']) && is_array($file_data['decorators'])) { - foreach ($file_data['decorators'] as $decorator) { - // Decorator format: [0 => 'decorator_name', 1 => [arguments]] - if (isset($decorator[0]) && $decorator[0] === 'route') { - // First argument is the route pattern - if (isset($decorator[1][0])) { - $route_pattern = $decorator[1][0]; - break; - } + // Generate URL from selected pattern + return static::_generate_url_from_pattern($selected_route['pattern'], $params_array, $class_name, '(SPA action)'); + } + + /** + * Select the best matching route from available routes based on provided parameters + * + * Selection algorithm: + * 1. Filter routes where all required parameters can be satisfied by provided params + * 2. Among satisfiable routes, prioritize those with MORE parameters (more specific) + * 3. If tie, any route works (deterministic by using first match) + * + * @param array $routes Array of route data from manifest + * @param array $params_array Provided parameters + * @return array|null Selected route data or null if none match + */ + protected static function _select_best_route(array $routes, array $params_array): ?array + { + $satisfiable = []; + + foreach ($routes as $route) { + $pattern = $route['pattern']; + + // Extract required parameters from pattern + $required_params = []; + if (preg_match_all('/:([a-zA-Z_][a-zA-Z0-9_]*)/', $pattern, $matches)) { + $required_params = $matches[1]; + } + + // Check if all required parameters are provided + $can_satisfy = true; + foreach ($required_params as $required) { + if (!array_key_exists($required, $params_array)) { + $can_satisfy = false; + break; } } + + if ($can_satisfy) { + $satisfiable[] = [ + 'route' => $route, + 'param_count' => count($required_params), + ]; + } } - if (!$route_pattern) { - throw new Rsx_Caller_Exception("SPA action {$class_name} must have @route() decorator with pattern"); + if (empty($satisfiable)) { + return null; } - // Generate URL from pattern using same logic as regular routes - return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, '(SPA action)'); + // Sort by parameter count descending (most parameters first) + usort($satisfiable, function ($a, $b) { + return $b['param_count'] <=> $a['param_count']; + }); + + // Return the route with the most parameters + return $satisfiable[0]['route']; } /** diff --git a/app/RSpade/Core/SPA/Spa_Layout.js b/app/RSpade/Core/SPA/Spa_Layout.js index 183911962..cdbb71773 100755 --- a/app/RSpade/Core/SPA/Spa_Layout.js +++ b/app/RSpade/Core/SPA/Spa_Layout.js @@ -90,6 +90,9 @@ class Spa_Layout extends Component { // Create new action component console.log('[Spa_Layout] Creating action:', action_name, 'with args:', args); console.log('[Spa_Layout] Args keys:', Object.keys(args || {})); + + console.warn(args); + const action = $content.component(action_name, args).component(); // Store reference diff --git a/app/RSpade/Core/SPA/Spa_ManifestSupport.php b/app/RSpade/Core/SPA/Spa_ManifestSupport.php index d3e6f77bc..c72cbc519 100755 --- a/app/RSpade/Core/SPA/Spa_ManifestSupport.php +++ b/app/RSpade/Core/SPA/Spa_ManifestSupport.php @@ -30,10 +30,13 @@ class Spa_ManifestSupport extends ManifestSupport_Abstract */ public static function process(array &$manifest_data): void { - // Initialize routes key if not already set + // Initialize routes structures if not already set if (!isset($manifest_data['data']['routes'])) { $manifest_data['data']['routes'] = []; } + if (!isset($manifest_data['data']['routes_by_target'])) { + $manifest_data['data']['routes_by_target'] = []; + } // Get all files to look up PHP controller metadata $files = $manifest_data['data']['files']; @@ -114,8 +117,8 @@ class Spa_ManifestSupport extends ManifestSupport_Abstract ); } - // Store route with unified structure - $manifest_data['data']['routes'][$route_pattern] = [ + // Store route with unified structure (for dispatcher) + $route_data = [ 'methods' => ['GET'], // Spa routes are always GET 'type' => 'spa', 'class' => $php_controller_fqcn, @@ -124,7 +127,17 @@ class Spa_ManifestSupport extends ManifestSupport_Abstract 'file' => $php_controller_file, 'require' => $require_attrs, 'js_action_class' => $class_name, + 'pattern' => $route_pattern, ]; + + $manifest_data['data']['routes'][$route_pattern] = $route_data; + + // Also store by target for URL generation (group multiple routes per action class) + $target = $class_name; // For SPA, target is the JS action class name + if (!isset($manifest_data['data']['routes_by_target'][$target])) { + $manifest_data['data']['routes_by_target'][$target] = []; + } + $manifest_data['data']['routes_by_target'][$target][] = $route_data; } } } diff --git a/app/RSpade/man/routing.txt b/app/RSpade/man/routing.txt index 94673938a..2304308fa 100755 --- a/app/RSpade/man/routing.txt +++ b/app/RSpade/man/routing.txt @@ -66,6 +66,24 @@ ROUTE PATTERNS $user_id = $params['id']; } + Multiple Routes Per Action: + Controllers and SPA actions can define multiple route patterns. + The best matching route is automatically selected based on provided parameters: + + #[Route('/users/add')] + #[Route('/users/:id/edit')] + public static function form(Request $request, array $params = []) + { + $user_id = $params['id'] ?? null; + // Handle both add and edit in single method + } + + Rsx::Route('User_Controller::form') // /users/add + Rsx::Route('User_Controller::form', ['id' => 5]) // /users/5/edit + + The framework selects the most specific route that satisfies all + required parameters. SPA actions use @route() decorator syntax. + Parameter Syntax: - :param - Required parameter in URL path - Routes without parameters use controller/action mapping diff --git a/app/RSpade/man/rsx_debug.txt b/app/RSpade/man/rsx_debug.txt new file mode 100755 index 000000000..316086f14 --- /dev/null +++ b/app/RSpade/man/rsx_debug.txt @@ -0,0 +1,247 @@ +RSX:DEBUG - Route Debugging Tool +================================ + +OVERVIEW + +rsx:debug is a comprehensive debugging tool that uses a headless Chromium browser +via Playwright to test routes, capture responses, monitor console output, inspect +DOM state, track XHR requests, and analyze logs. It provides complete visibility +into both server-side and client-side behavior. + +This is a development-only tool that is disabled in production environments. + +BASIC USAGE + + php artisan rsx:debug [options] + +Examples: + php artisan rsx:debug /dashboard + php artisan rsx:debug /api/users --no-body + php artisan rsx:debug /login --full + +CORE OPTIONS + +--user= | --user-id= + Test as a specific user ID, bypassing authentication. Uses backdoor + authentication that only works in development environments. + +--no-body + Suppress HTTP response body output. Useful when you only want to see + headers, status code, console errors, or other diagnostic information. + +--full + Enable all display options except --no-body and --follow-redirects. + Provides maximum diagnostic information in a single command. + +--timeout= + Navigation timeout in milliseconds (minimum 30000ms, default 30000ms). + Use for slow-loading pages or complex SPAs. + +CONSOLE OUTPUT + +--console-log | --console-list + Display all browser console output, not just errors. Shows console.log(), + console.warn(), console.info(), and console_debug() output. + +--console-debug-filter= + Filter console_debug() output to a specific channel (e.g., AUTH, DISPATCH, + BENCHMARK). Automatically enables console_debug and --console-log. + +--console-debug-all + Show all console_debug() channels without filtering. + +--console-debug-benchmark + Include benchmark timing prefixes in console_debug() output. + +--console-debug-disable + Disable console_debug() entirely for this test. + +LOGS + +--log + Display Laravel error log if not empty. + +--all-logs + Display all log files: Laravel log and nginx access/error logs. + +XHR/AJAX MONITORING + +--xhr-list + Show simple list of XHR/fetch requests with URLs and status codes only. + +--xhr-dump + Capture full details of XHR/fetch requests including headers, request + payload, and response body. + +DOM INSPECTION + +--expect-element= + Verify that an element exists by CSS selector. Command fails if element + is not found. Useful for testing that critical UI elements render. + +--dump-element= + Extract and display HTML of a specific element by CSS selector. + +--input-elements + List all form input elements on the page with their values, attributes, + and state. + +--wait-for= + Wait for a CSS selector to appear before capturing output. Useful for + pages with async content loading or SPAs. + +HTTP TESTING + +--post= + Send POST request with JSON data instead of GET request. + Example: --post='{"username":"test","password":"pass"}' + +--follow-redirects + Follow HTTP redirects and show the full redirect chain. + +--headers + Display all HTTP response headers. + +--cookies + Display all browser cookies with domains and expiry times. + +BROWSER STATE + +--storage + Display localStorage and sessionStorage contents. + +SCREENSHOTS + +--screenshot-path= + Path to save screenshot file. Triggers screenshot capture with default + width of 1920px and maximum height of 5000px. + +--screenshot-width= + Screenshot width in pixels or device preset name. Used with --screenshot-path. + + Available presets: + - mobile 412px (Pixel 7) + - iphone-mobile 390px (iPhone 12/13/14) + - tablet 768px (iPad Mini) + - desktop-small 1366px (Common laptop) + - desktop-medium 1920px (Full HD) - default + - desktop-large 2560px (2K/WQHD) + + Examples: + --screenshot-path=/tmp/page.png + # Default 1920px width + + --screenshot-width=mobile --screenshot-path=/tmp/mobile.png + # 412px mobile viewport + + --screenshot-width=1024 --screenshot-path=/tmp/custom.png + # Custom 1024px width + +JAVASCRIPT EVALUATION + +--eval= + Execute JavaScript code in the page context and display the result. + Useful for testing RSX JavaScript APIs or inspecting global state. + + Examples: + --eval="Rsx.Route('Demo_Controller').url()" + --eval="JSON.stringify(Rsx._routes)" + --eval="typeof jQuery" + --eval="document.title" + +AUTHENTICATION & BACKDOOR + +The --user and --user-id options use backdoor authentication that only works +in development/testing environments. The tool sends an X-Dev-Auth-User-Id +header that the framework recognizes and uses to authenticate as that user +without requiring login credentials. + +This feature is: +- Only available in local/development/testing environments +- Automatically disabled in production +- Useful for testing protected routes +- Does not require modifying session state + +ERROR HANDLING + +The tool sends an X-Playwright-Test header that triggers plain text error +responses instead of HTML error pages. This provides cleaner error output +with full stack traces that are easier to debug. + +Exit codes: +- 0: Success (status < 400, no console errors, no network failures) +- 1: Failure (status >= 400, console errors, or network failures) + +OUTPUT FORMAT + +The command outputs in a terse, parseable format: +- Status line with URL and HTTP status code +- Console errors (always shown if present) +- Console logs (if --console-log used) +- XHR/fetch requests (if --xhr-dump or --xhr-list used) +- Response headers (if --headers used) +- Response body (unless --no-body used) +- Laravel/nginx logs (if --log or --all-logs used) + +PREREQUISITES + +The tool automatically installs required dependencies: +- Node.js (must be pre-installed) +- Playwright npm package (auto-installed if missing) +- Chromium browser (auto-installed if missing) + +LOG ROTATION + +The tool automatically rotates development logs before and after each test +to ensure a clean slate. This means each test starts with empty logs and +only shows errors/output from that specific test run. + +COMMON PATTERNS + +Test a protected route as user ID 1: + php artisan rsx:debug /admin/users --user=1 + +Check if JavaScript errors occur: + php artisan rsx:debug /page + # Console errors are always shown + +Monitor all AJAX calls on a page: + php artisan rsx:debug /dashboard --xhr-list + +Debug a form submission: + php artisan rsx:debug /api/save --post='{"name":"test"}' --xhr-dump + +Verify critical elements loaded: + php artisan rsx:debug /page --expect-element="#submit-button" + +Check console_debug output for AUTH channel: + php artisan rsx:debug /login --console-debug-filter=AUTH + +Capture mobile and desktop screenshots: + php artisan rsx:debug /page --screenshot-width=mobile --screenshot-path=/tmp/mobile.png + php artisan rsx:debug /page --screenshot-width=desktop-large --screenshot-path=/tmp/desktop.png + +IMPORTANT NOTES + +• When using rsx:debug with grep and no output appears, re-run without grep + to see the full context and any errors that may have occurred. + +• Use rsx_dump_die() in your PHP code for temporary debugging output. + +• This command is development-only and automatically disabled in production. + +• The tool uses a real Chromium browser, so JavaScript executes exactly as + it would for end users. + +• For more details on console_debug: php artisan rsx:man console_debug + For config options: php artisan rsx:man config_rsx + +RELATED COMMANDS + +php artisan rsx:routes List all registered routes +php artisan rsx:ajax Test Ajax endpoints directly +php artisan rsx:check Run code quality checks + +SEE ALSO + +console_debug, config_rsx, routing diff --git a/app/RSpade/man/spa.txt b/app/RSpade/man/spa.txt index 4a62deca4..be675c559 100755 --- a/app/RSpade/man/spa.txt +++ b/app/RSpade/man/spa.txt @@ -231,6 +231,12 @@ JAVASCRIPT ACTIONS URL pattern with optional :param segments Example: '/contacts/:id', '/users/:id/posts/:post_id' + Multiple routes per action: + @route('/contacts/add') + @route('/contacts/:id/edit') + The best matching route is selected based on provided parameters. + Enables single action to handle add/edit in one class. + @layout(class_name) Layout class to render within Example: 'Frontend_Layout', 'Dashboard_Layout' diff --git a/bin/route-debug.js b/bin/route-debug.js index f19c31637..afd06a277 100755 --- a/bin/route-debug.js +++ b/bin/route-debug.js @@ -59,6 +59,16 @@ function parse_args() { process.exit(0); } + // Device viewport presets + const device_presets = { + 'mobile': 412, // Pixel 7 + 'iphone-mobile': 390, // iPhone 12/13/14 + 'tablet': 768, // iPad Mini + 'desktop-small': 1366, // Common laptop + 'desktop-medium': 1920, // Full HD + 'desktop-large': 2560 // 2K/WQHD + }; + const options = { route: null, user_id: null, @@ -83,7 +93,9 @@ function parse_args() { console_debug_filter: null, console_debug_benchmark: false, console_debug_all: false, - console_debug_disable: false + console_debug_disable: false, + screenshot_width: null, + screenshot_path: null }; for (const arg of args) { @@ -146,6 +158,16 @@ function parse_args() { options.console_debug_all = true; } else if (arg === '--console-debug-disable') { options.console_debug_disable = true; + } else if (arg.startsWith('--screenshot-width=')) { + const width_value = arg.substring(19); + // Check if it's a preset name or numeric value + if (device_presets[width_value]) { + options.screenshot_width = device_presets[width_value]; + } else { + options.screenshot_width = parseInt(width_value); + } + } else if (arg.startsWith('--screenshot-path=')) { + options.screenshot_path = arg.substring(18); } else if (!arg.startsWith('--')) { options.route = arg; } @@ -188,10 +210,23 @@ function parse_args() { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); - - const context = await browser.newContext({ + + // Set viewport for screenshot if requested + const contextOptions = { ignoreHTTPSErrors: true - }); + }; + + if (options.screenshot_path) { + // Default to 1920 if width not specified + const screenshot_width = options.screenshot_width || 1920; + contextOptions.viewport = { + width: screenshot_width, + height: 1080 // Will expand to full page height on screenshot + }; + options.screenshot_width = screenshot_width; // Store for later use + } + + const context = await browser.newContext(contextOptions); const page = await context.newPage(); @@ -1075,7 +1110,26 @@ function parse_args() { } } } - + + // Take screenshot if requested + if (options.screenshot_path) { + try { + await page.screenshot({ + path: options.screenshot_path, + fullPage: true, + clip: { + x: 0, + y: 0, + width: options.screenshot_width, + height: Math.min(5000, await page.evaluate(() => document.documentElement.scrollHeight)) + } + }); + console.log(`Screenshot saved to: ${options.screenshot_path} (width: ${options.screenshot_width}px)`); + } catch (e) { + console.error(`Failed to save screenshot: ${e.message}`); + } + } + await browser.close(); // Exit with appropriate code diff --git a/docs/CLAUDE.dist.md b/docs/CLAUDE.dist.md index 2e10d0cff..443a40419 100644 --- a/docs/CLAUDE.dist.md +++ b/docs/CLAUDE.dist.md @@ -1092,9 +1092,12 @@ Uses Playwright to render the page and show rendered output, JavaScript errors, rsx:debug /clients # Test route rsx:debug /dashboard --user=1 # Simulate authenticated user rsx:debug /contacts --console # Show console.log output +rsx:debug /page --screenshot-path=/tmp/page.png --screenshot-width=mobile # Capture screenshot rsx:debug /path --help # Show all options ``` +Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large + Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features. ### Debugging