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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
247
app/RSpade/man/rsx_debug.txt
Executable file
247
app/RSpade/man/rsx_debug.txt
Executable file
@@ -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 <url> [options]
|
||||
|
||||
Examples:
|
||||
php artisan rsx:debug /dashboard
|
||||
php artisan rsx:debug /api/users --no-body
|
||||
php artisan rsx:debug /login --full
|
||||
|
||||
CORE OPTIONS
|
||||
|
||||
--user=<id> | --user-id=<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=<ms>
|
||||
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=<channel>
|
||||
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=<selector>
|
||||
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=<selector>
|
||||
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=<selector>
|
||||
Wait for a CSS selector to appear before capturing output. Useful for
|
||||
pages with async content loading or SPAs.
|
||||
|
||||
HTTP TESTING
|
||||
|
||||
--post=<json>
|
||||
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>
|
||||
Path to save screenshot file. Triggers screenshot capture with default
|
||||
width of 1920px and maximum height of 5000px.
|
||||
|
||||
--screenshot-width=<width|preset>
|
||||
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=<code>
|
||||
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
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user