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>
1140 lines
44 KiB
JavaScript
Executable File
1140 lines
44 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* RSX Route Debug Script
|
|
* Debugs a single route using Playwright
|
|
*
|
|
* Usage: node route-debug.js <route> [options]
|
|
*
|
|
* Arguments:
|
|
* route The route to debug (e.g., /dashboard)
|
|
*
|
|
* Options:
|
|
* --user-id=<id> Test as specific user ID
|
|
* --log Always display Laravel error log
|
|
* --no-body Suppress body output
|
|
* --follow-redirects Follow redirects and show redirect chain
|
|
* --help Show this help message
|
|
*/
|
|
|
|
const { chromium } = require('playwright');
|
|
const fs = require('fs');
|
|
|
|
// Parse command line arguments
|
|
function parse_args() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length === 0 || args.includes('--help')) {
|
|
console.log('RSX Route Debug - Debug a single route using Playwright');
|
|
console.log('');
|
|
console.log('Usage: node route-debug.js <route> [options]');
|
|
console.log('');
|
|
console.log('Arguments:');
|
|
console.log(' route The route to debug (e.g., /dashboard)');
|
|
console.log('');
|
|
console.log('Options:');
|
|
console.log(' --user-id=<id> Test as specific user ID');
|
|
console.log(' --log Always display Laravel error log');
|
|
console.log(' --no-body Suppress body output');
|
|
console.log(' --follow-redirects Follow redirects and show redirect chain');
|
|
console.log(' --headers Display all response headers');
|
|
console.log(' --console-log Display all console output (not just errors) and console_debug() output even if SHOW_CONSOLE_DEBUG_HTTP=false');
|
|
console.log(' --xhr-dump Full XHR/fetch dump (headers, payload, response)');
|
|
console.log(' --xhr-list Simple XHR list (URLs and status codes only)');
|
|
console.log(' --input-elements List all form input elements on the page');
|
|
console.log(' --post=<data> Send POST request with JSON data (e.g., --post=\'{"key":"value"}\')');
|
|
console.log(' --cookies Display all cookies');
|
|
console.log(' --wait-for=<sel> Wait for element selector before capturing');
|
|
console.log(' --all-logs Display all log files (Laravel, nginx access/error)');
|
|
console.log(' --expect-element=<sel> Verify element exists (fail if not found)');
|
|
console.log(' --dump-element=<sel> Extract and display HTML of specific element');
|
|
console.log(' --storage Dump localStorage and sessionStorage contents');
|
|
console.log(' --full Enable all display options for maximum info');
|
|
console.log(' --eval=<code> Execute JavaScript code in the page context');
|
|
console.log(' --timeout=<ms> Navigation timeout in milliseconds (minimum 30000ms, default 30000ms)');
|
|
console.log(' --console-debug-filter=<ch> Filter console_debug to specific channel');
|
|
console.log(' --console-debug-benchmark Include benchmark timing in console_debug');
|
|
console.log(' --console-debug-all Show all console_debug channels');
|
|
console.log(' --help Show this help message');
|
|
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,
|
|
show_log: false,
|
|
no_body: false,
|
|
follow_redirects: false,
|
|
headers: false,
|
|
console_log: false,
|
|
xhr_dump: false,
|
|
xhr_list: false,
|
|
input_elements: false,
|
|
post_data: null,
|
|
cookies: false,
|
|
wait_for: null,
|
|
all_logs: false,
|
|
expect_element: null,
|
|
dump_element: null,
|
|
storage: false,
|
|
eval_code: null,
|
|
verbose: false,
|
|
timeout: 30000,
|
|
console_debug_filter: null,
|
|
console_debug_benchmark: false,
|
|
console_debug_all: false,
|
|
console_debug_disable: false,
|
|
screenshot_width: null,
|
|
screenshot_path: null
|
|
};
|
|
|
|
for (const arg of args) {
|
|
if (arg.startsWith('--user-id=')) {
|
|
options.user_id = arg.split('=')[1];
|
|
} else if (arg === '--log') {
|
|
options.show_log = true;
|
|
} else if (arg === '--no-body') {
|
|
options.no_body = true;
|
|
} else if (arg === '--follow-redirects') {
|
|
options.follow_redirects = true;
|
|
} else if (arg === '--headers') {
|
|
options.headers = true;
|
|
} else if (arg === '--console-log') {
|
|
options.console_log = true;
|
|
} else if (arg === '--xhr-dump') {
|
|
options.xhr_dump = true;
|
|
} else if (arg === '--xhr-list') {
|
|
options.xhr_list = true;
|
|
} else if (arg === '--input-elements') {
|
|
options.input_elements = true;
|
|
} else if (arg.startsWith('--post=')) {
|
|
const postData = arg.substring(7); // Remove '--post=' prefix
|
|
try {
|
|
// Try to parse as JSON
|
|
options.post_data = JSON.parse(postData);
|
|
} catch (e) {
|
|
// If not valid JSON, treat as form-encoded string
|
|
options.post_data = postData;
|
|
}
|
|
} else if (arg === '--cookies') {
|
|
options.cookies = true;
|
|
} else if (arg.startsWith('--wait-for=')) {
|
|
options.wait_for = arg.substring(11);
|
|
} else if (arg === '--all-logs') {
|
|
options.all_logs = true;
|
|
} else if (arg.startsWith('--expect-element=')) {
|
|
options.expect_element = arg.substring(17);
|
|
} else if (arg.startsWith('--dump-element=')) {
|
|
options.dump_element = arg.substring(15);
|
|
} else if (arg === '--storage') {
|
|
options.storage = true;
|
|
} else if (arg === '--full') {
|
|
options.full = true;
|
|
} else if (arg.startsWith('--eval=')) {
|
|
// Get the eval code after the equals sign
|
|
let evalCode = arg.substring(7);
|
|
options.eval_code = evalCode;
|
|
} else if (arg.startsWith('--timeout=')) {
|
|
options.timeout = parseInt(arg.substring(10));
|
|
if (options.timeout < 30000) {
|
|
console.error('Error: Timeout value is in milliseconds and must be no less than 30000 milliseconds (30 seconds)');
|
|
process.exit(1);
|
|
}
|
|
} else if (arg.startsWith('--console-debug-filter=')) {
|
|
options.console_debug_filter = arg.substring(23);
|
|
} else if (arg === '--console-debug-benchmark') {
|
|
options.console_debug_benchmark = true;
|
|
} else if (arg === '--console-debug-all') {
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (!options.route) {
|
|
console.error('Error: Route argument is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Ensure route starts with /
|
|
if (!options.route.startsWith('/')) {
|
|
options.route = '/' + options.route;
|
|
}
|
|
|
|
// If full mode, enable all display options (except no-body and follow-redirects)
|
|
if (options.full) {
|
|
options.headers = true;
|
|
options.console_log = true;
|
|
options.xhr_dump = true;
|
|
options.input_elements = true;
|
|
options.cookies = true;
|
|
options.all_logs = true;
|
|
options.storage = true;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
// Main execution
|
|
(async () => {
|
|
const options = parse_args();
|
|
|
|
const baseUrl = 'http://localhost';
|
|
const fullUrl = baseUrl + options.route;
|
|
const laravel_log_path = process.env.LARAVEL_LOG_PATH || '/var/www/html/storage/logs/laravel.log';
|
|
|
|
// Launch browser (always headless)
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
});
|
|
|
|
// 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();
|
|
|
|
// Collect console messages
|
|
const consoleErrors = [];
|
|
const consoleMessages = [];
|
|
|
|
// Collect uncaught page errors (exceptions)
|
|
page.on('pageerror', error => {
|
|
const errorMsg = `[UNCAUGHT] ${error.message}`;
|
|
const stack = error.stack ? error.stack.split('\n').slice(1, 3).join('\n') : '';
|
|
consoleErrors.push(errorMsg + (stack ? '\n' + stack : ''));
|
|
});
|
|
|
|
page.on('console', async msg => {
|
|
const type = msg.type();
|
|
const text = msg.text();
|
|
const location = msg.location();
|
|
|
|
// Get stack trace for errors and warnings
|
|
let stackTrace = [];
|
|
if (type === 'error' || type === 'warning') {
|
|
try {
|
|
const trace = msg.stackTrace();
|
|
if (trace && trace.length > 0) {
|
|
// Get up to 3 stack frames
|
|
stackTrace = trace.slice(0, 3).map(frame => {
|
|
if (frame.url && !frame.url.startsWith('http://localhost')) {
|
|
return ` at ${frame.url}:${frame.lineNumber}:${frame.columnNumber}`;
|
|
} else if (frame.url && frame.url.includes('.js')) {
|
|
const urlPath = frame.url.replace('http://localhost', '');
|
|
return ` at ${urlPath}:${frame.lineNumber}:${frame.columnNumber}`;
|
|
} else if (frame.functionName) {
|
|
return ` at ${frame.functionName}`;
|
|
}
|
|
return null;
|
|
}).filter(Boolean);
|
|
}
|
|
} catch (e) {
|
|
// Stack trace not available
|
|
}
|
|
}
|
|
|
|
// Only include location for JS files, not inline HTML scripts
|
|
let locationStr = '';
|
|
if (location && location.url && !location.url.startsWith('http://localhost')) {
|
|
// External JS file or file:// URL
|
|
locationStr = `${location.url}:${location.lineNumber}`;
|
|
} else if (location && location.url && location.url.includes('.js')) {
|
|
// Local JS file (not inline HTML)
|
|
const urlPath = location.url.replace('http://localhost', '');
|
|
locationStr = `${urlPath}:${location.lineNumber}`;
|
|
}
|
|
|
|
// Format the message with type label for errors/warnings
|
|
let formattedMsg = text;
|
|
if (type === 'error') {
|
|
formattedMsg = `[ERROR] ${locationStr ? `[${locationStr}] ` : ''}${text}`;
|
|
if (stackTrace.length > 0) {
|
|
formattedMsg += '\n' + stackTrace.join('\n');
|
|
}
|
|
consoleErrors.push(formattedMsg);
|
|
} else if (type === 'warning') {
|
|
formattedMsg = `[WARN] ${locationStr ? `[${locationStr}] ` : ''}${text}`;
|
|
if (stackTrace.length > 0) {
|
|
formattedMsg += '\n' + stackTrace.join('\n');
|
|
}
|
|
}
|
|
|
|
if (options.console_log) {
|
|
consoleMessages.push({
|
|
type: type,
|
|
text: formattedMsg,
|
|
location: locationStr
|
|
});
|
|
}
|
|
});
|
|
|
|
// Collect network failures
|
|
const networkFailures = [];
|
|
page.on('requestfailed', request => {
|
|
networkFailures.push(request.url());
|
|
});
|
|
|
|
// Collect XHR/fetch requests and responses
|
|
const xhrRequests = [];
|
|
if (options.xhr_dump || options.xhr_list) {
|
|
page.on('request', request => {
|
|
const resourceType = request.resourceType();
|
|
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
|
xhrRequests.push({
|
|
url: request.url(),
|
|
method: request.method(),
|
|
headers: request.headers(),
|
|
postData: request.postData(),
|
|
response: null
|
|
});
|
|
}
|
|
});
|
|
|
|
page.on('response', response => {
|
|
const request = response.request();
|
|
const resourceType = request.resourceType();
|
|
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
|
const xhrRequest = xhrRequests.find(r => r.url === request.url() && !r.response);
|
|
if (xhrRequest) {
|
|
xhrRequest.response = {
|
|
status: response.status(),
|
|
headers: response.headers(),
|
|
body: null
|
|
};
|
|
// Try to get response body
|
|
response.text().then(body => {
|
|
xhrRequest.response.body = body;
|
|
}).catch(() => {
|
|
// Ignore errors getting body
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Track redirect chain
|
|
const redirectChain = [];
|
|
if (options.follow_redirects) {
|
|
page.on('response', response => {
|
|
const status = response.status();
|
|
if (status >= 300 && status < 400) {
|
|
const location = response.headers()['location'];
|
|
if (location) {
|
|
redirectChain.push({
|
|
url: response.url(),
|
|
status: status,
|
|
location: location
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set up headers for initial request only
|
|
const extraHeaders = {};
|
|
if (options.user_id) {
|
|
extraHeaders['X-Dev-Auth-User-Id'] = options.user_id;
|
|
}
|
|
// Add Playwright test header to get text errors
|
|
extraHeaders['X-Playwright-Test'] = '1';
|
|
// Add console debug header if console logging is requested
|
|
if (options.console_log || options.full) {
|
|
extraHeaders['X-Playwright-Console-Debug'] = '1';
|
|
}
|
|
// Add console debug filter headers
|
|
if (options.console_debug_filter) {
|
|
extraHeaders['X-Console-Debug-Filter'] = options.console_debug_filter;
|
|
}
|
|
if (options.console_debug_benchmark) {
|
|
extraHeaders['X-Console-Debug-Benchmark'] = '1';
|
|
}
|
|
if (options.console_debug_all) {
|
|
extraHeaders['X-Console-Debug-All'] = '1';
|
|
}
|
|
if (options.console_debug_disable) {
|
|
extraHeaders['X-Console-Debug-Disable'] = '1';
|
|
}
|
|
|
|
// Use route interception to only add headers to main request
|
|
await page.route('**/*', async (route, request) => {
|
|
const url = request.url();
|
|
// Only add headers to our local requests, not CDN requests
|
|
if (url.startsWith('http://localhost')) {
|
|
await route.continue({
|
|
headers: {
|
|
...request.headers(),
|
|
...extraHeaders
|
|
}
|
|
});
|
|
} else {
|
|
// For external requests, continue without custom headers
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// Navigate to the route with retry logic for connection refused
|
|
let response;
|
|
let lastError;
|
|
const maxRetries = 3;
|
|
|
|
// Handle POST requests differently
|
|
if (options.post_data) {
|
|
// Navigate to a blank page first
|
|
await page.goto('about:blank');
|
|
|
|
// Make POST request using fetch in the page context
|
|
const fetchResult = await page.evaluate(async ({url, data, headers}) => {
|
|
let body;
|
|
let contentType;
|
|
|
|
if (typeof data === 'object') {
|
|
// Send as JSON
|
|
body = JSON.stringify(data);
|
|
contentType = 'application/json';
|
|
} else {
|
|
// Send as form-encoded
|
|
body = data;
|
|
contentType = 'application/x-www-form-urlencoded';
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': contentType,
|
|
...headers
|
|
},
|
|
body: body
|
|
});
|
|
|
|
const text = await response.text();
|
|
return {
|
|
status: response.status,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
body: text,
|
|
url: response.url
|
|
};
|
|
}, {url: fullUrl, data: options.post_data, headers: extraHeaders});
|
|
|
|
// Create a mock response object similar to page.goto response
|
|
response = {
|
|
status: () => fetchResult.status,
|
|
headers: () => fetchResult.headers,
|
|
text: async () => fetchResult.body,
|
|
url: () => fetchResult.url
|
|
};
|
|
|
|
// If response contains HTML, set it as page content for further analysis
|
|
if (fetchResult.headers['content-type'] && fetchResult.headers['content-type'].includes('text/html')) {
|
|
await page.setContent(fetchResult.body, {waitUntil: 'networkidle'});
|
|
}
|
|
} else {
|
|
// GET request with retries
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
response = await page.goto(fullUrl, {
|
|
waitUntil: 'networkidle',
|
|
timeout: options.timeout
|
|
});
|
|
break; // Success, exit retry loop
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
// Check if it's a connection refused error
|
|
if (error.message.includes('ERR_CONNECTION_REFUSED')) {
|
|
if (attempt < maxRetries) {
|
|
// Wait before retrying (exponential backoff: 1s, 2s, 4s)
|
|
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
console.error(`Connection refused, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Not a connection refused error or max retries reached
|
|
console.log(`FAIL ${fullUrl} - Navigation error: ${error.message}`);
|
|
await browser.close();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!response) {
|
|
console.log(`FAIL ${fullUrl} - Navigation error after ${maxRetries} attempts: ${lastError.message}`);
|
|
await browser.close();
|
|
process.exit(1);
|
|
}
|
|
|
|
// Wait for RSX framework and jqhtml components to complete initialization
|
|
// This ensures all components have gone through their lifecycle (render -> create -> load -> ready)
|
|
await page.evaluate(() => {
|
|
return new Promise((resolve) => {
|
|
// Check if RSX framework with _debug_ready event is available
|
|
if (window.Rsx && window.Rsx.on) {
|
|
// Use Rsx._debug_ready event which fires after all jqhtml components complete lifecycle
|
|
window.Rsx.on('_debug_ready', function() {
|
|
resolve();
|
|
});
|
|
} else {
|
|
// Fallback for non-RSX pages: wait for DOMContentLoaded + 200ms
|
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
setTimeout(function() { resolve(); }, 200);
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setTimeout(function() { resolve(); }, 200);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Wait for specific element if requested
|
|
if (options.wait_for) {
|
|
try {
|
|
await page.waitForSelector(options.wait_for, {
|
|
timeout: options.timeout
|
|
});
|
|
} catch (e) {
|
|
console.log(`Warning: Element '${options.wait_for}' not found within 10 seconds`);
|
|
}
|
|
}
|
|
|
|
// Verify element exists if --expect-element is passed
|
|
if (options.expect_element) {
|
|
const elementExists = await page.evaluate((selector) => {
|
|
return document.querySelector(selector) !== null;
|
|
}, options.expect_element);
|
|
|
|
if (!elementExists) {
|
|
console.log(`FAIL: Expected element '${options.expect_element}' not found on page`);
|
|
await browser.close();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Dump specific element HTML if --dump-element is passed
|
|
if (options.dump_element) {
|
|
const elementHTML = await page.evaluate((selector) => {
|
|
const elem = document.querySelector(selector);
|
|
if (elem) {
|
|
return elem.outerHTML;
|
|
}
|
|
return null;
|
|
}, options.dump_element);
|
|
|
|
if (elementHTML) {
|
|
console.log(`\nElement HTML for '${options.dump_element}':`);
|
|
console.log(elementHTML);
|
|
console.log('');
|
|
} else {
|
|
console.log(`Warning: Element '${options.dump_element}' not found for dumping`);
|
|
}
|
|
}
|
|
|
|
// Execute eval code if --eval option is passed
|
|
if (options.eval_code) {
|
|
try {
|
|
const evalResult = await page.evaluate((code) => {
|
|
return new Promise((resolve) => {
|
|
// Wrap the eval code in a function
|
|
const __eval_func = function() {
|
|
try {
|
|
// Use eval directly for more complex expressions
|
|
const result = eval(code);
|
|
|
|
// Convert result to string representation
|
|
if (result === undefined) {
|
|
return 'undefined';
|
|
} else if (result === null) {
|
|
return 'null';
|
|
} else if (typeof result === 'object') {
|
|
try {
|
|
return JSON.stringify(result, null, 2);
|
|
} catch (e) {
|
|
return String(result);
|
|
}
|
|
} else {
|
|
return String(result);
|
|
}
|
|
} catch (error) {
|
|
return `Error: ${error.message}`;
|
|
}
|
|
};
|
|
|
|
// Wait for RSX framework to be fully initialized
|
|
if (window.Rsx && window.Rsx.on) {
|
|
// Use Rsx._debug_ready event which fires after all initialization
|
|
window.Rsx.on('_debug_ready', function() {
|
|
resolve(__eval_func());
|
|
});
|
|
} else {
|
|
// Fallback for non-RSX pages
|
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
setTimeout(function() { resolve(__eval_func()); }, 500);
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setTimeout(function() { resolve(__eval_func()); }, 500);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}, options.eval_code);
|
|
|
|
console.log('\nJavaScript Eval Result:');
|
|
console.log(evalResult);
|
|
console.log('');
|
|
} catch (error) {
|
|
console.log('\nJavaScript Eval Error:');
|
|
console.log(error.message);
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
// Get response details
|
|
const status = response.status();
|
|
const headers_response = response.headers();
|
|
// Get the live DOM content after JavaScript execution, not the initial HTTP response
|
|
const body = await page.content();
|
|
|
|
// Build output line
|
|
let output = `${status} ${fullUrl}`;
|
|
|
|
if (options.post_data) {
|
|
output += ` method:POST`;
|
|
}
|
|
|
|
if (options.user_id) {
|
|
output += ` user:${options.user_id}`;
|
|
}
|
|
|
|
// Add key headers
|
|
if (headers_response['content-type']) {
|
|
output += ` type:${headers_response['content-type'].split(';')[0]}`;
|
|
}
|
|
|
|
if (headers_response['x-frame-options']) {
|
|
output += ` x-frame:${headers_response['x-frame-options']}`;
|
|
}
|
|
|
|
// Add errors if any
|
|
if (consoleErrors.length > 0) {
|
|
output += ` console-errors:${consoleErrors.length}`;
|
|
}
|
|
|
|
if (networkFailures.length > 0) {
|
|
output += ` network-failures:${networkFailures.length}`;
|
|
}
|
|
|
|
// Output single log line
|
|
console.log(output);
|
|
|
|
// Show redirect chain if --follow-redirects was used
|
|
if (options.follow_redirects && redirectChain.length > 0) {
|
|
console.log('');
|
|
console.log('Redirect Chain:');
|
|
redirectChain.forEach((redirect, index) => {
|
|
console.log(` ${index + 1}. ${redirect.status} ${redirect.url} -> ${redirect.location}`);
|
|
});
|
|
console.log(` ${redirectChain.length + 1}. ${status} ${response.url()} (final)`);
|
|
} else {
|
|
// Show redirect location if this is a redirect and we're not following
|
|
const redirect_codes = [301, 302, 303, 307, 308];
|
|
if (redirect_codes.includes(status) && headers_response['location']) {
|
|
console.log(`Redirect Location: ${headers_response['location']}`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// Check for JavaScript syntax errors - these are critical and should be shown prominently
|
|
const syntaxErrors = consoleErrors.filter(error =>
|
|
error.includes('SyntaxError') ||
|
|
error.includes('Unexpected token') ||
|
|
error.includes('expected expression')
|
|
);
|
|
|
|
if (syntaxErrors.length > 0) {
|
|
// For syntax errors, make this the primary output regardless of flags
|
|
console.log('===============================================');
|
|
console.log('CRITICAL: JavaScript Syntax Error Detected');
|
|
console.log('===============================================');
|
|
console.log('');
|
|
console.log('The page cannot load properly due to JavaScript syntax errors:');
|
|
console.log('');
|
|
for (const error of syntaxErrors) {
|
|
// Extract file and line info if present
|
|
const fileMatch = error.match(/([^:\s]+\.js):(\d+):?(\d+)?/);
|
|
if (fileMatch) {
|
|
console.log(`File: ${fileMatch[1]}`);
|
|
console.log(`Line: ${fileMatch[2]}${fileMatch[3] ? ', Column: ' + fileMatch[3] : ''}`);
|
|
console.log('');
|
|
}
|
|
|
|
// Output the full error
|
|
const lines = error.split('\n');
|
|
lines.forEach(line => console.log(line));
|
|
}
|
|
console.log('');
|
|
console.log('===============================================');
|
|
console.log('Fix the syntax error above before proceeding.');
|
|
console.log('Other debug output suppressed due to critical error.');
|
|
console.log('===============================================');
|
|
|
|
// Exit early for syntax errors
|
|
process.exit(1);
|
|
}
|
|
|
|
// Show JavaScript console messages or errors (normal flow)
|
|
if (options.console_log && consoleMessages.length > 0) {
|
|
console.log('JavaScript Console Output:');
|
|
for (const msg of consoleMessages) {
|
|
// For errors and warnings, text already includes the type prefix
|
|
if (msg.type === 'error' || msg.type === 'warning') {
|
|
// Split on newlines to handle stack traces properly
|
|
const lines = msg.text.split('\n');
|
|
lines.forEach(line => console.log(` ${line}`));
|
|
} else {
|
|
console.log(` [${msg.type}] ${msg.text}`);
|
|
}
|
|
}
|
|
} else if (consoleErrors.length > 0) {
|
|
console.log('JavaScript Console Errors:');
|
|
for (const error of consoleErrors) {
|
|
// Split on newlines to handle stack traces properly
|
|
const lines = error.split('\n');
|
|
lines.forEach(line => console.log(` ${line}`));
|
|
}
|
|
} else {
|
|
console.log('JavaScript Console Errors: None');
|
|
}
|
|
console.log('');
|
|
|
|
// Show response headers if --headers flag is passed
|
|
if (options.headers) {
|
|
console.log('Response Headers:');
|
|
const sorted_headers = Object.keys(headers_response).sort();
|
|
for (const header of sorted_headers) {
|
|
console.log(` ${header}: ${headers_response[header]}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Show network failures if any
|
|
if (networkFailures.length > 0) {
|
|
console.log('Network Failures:');
|
|
for (const url of networkFailures) {
|
|
console.log(` ${url}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Show XHR/fetch requests based on flags
|
|
if ((options.xhr_dump || options.xhr_list) && xhrRequests.length > 0) {
|
|
// Wait a bit for responses to complete
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
if (options.xhr_list) {
|
|
// Simple list mode
|
|
console.log('XHR/Fetch Requests (simple list):');
|
|
for (const xhr of xhrRequests) {
|
|
const status = xhr.response ? xhr.response.status : 'pending';
|
|
console.log(` ${xhr.method} ${xhr.url} - ${status}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
if (options.xhr_dump) {
|
|
// Full dump mode
|
|
console.log('XHR/Fetch Requests (full dump):');
|
|
for (const xhr of xhrRequests) {
|
|
console.log(` ${xhr.method} ${xhr.url}`);
|
|
console.log(' Request Headers:');
|
|
for (const [key, value] of Object.entries(xhr.headers)) {
|
|
console.log(` ${key}: ${value}`);
|
|
}
|
|
if (xhr.postData) {
|
|
console.log(` Request Body:`);
|
|
console.log(` ${xhr.postData}`);
|
|
}
|
|
if (xhr.response) {
|
|
console.log(` Response Status: ${xhr.response.status}`);
|
|
console.log(' Response Headers:');
|
|
for (const [key, value] of Object.entries(xhr.response.headers)) {
|
|
console.log(` ${key}: ${value}`);
|
|
}
|
|
if (xhr.response.body) {
|
|
console.log(` Response Body:`);
|
|
console.log(` ${xhr.response.body}`);
|
|
}
|
|
} else {
|
|
console.log(` Response: (pending or failed)`);
|
|
}
|
|
console.log('');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show input elements if --input-elements flag is passed
|
|
if (options.input_elements) {
|
|
const inputs = await page.evaluate(() => {
|
|
const elements = [];
|
|
|
|
// Find all input elements
|
|
document.querySelectorAll('input, select, textarea').forEach(elem => {
|
|
const info = {
|
|
tag: elem.tagName.toLowerCase(),
|
|
type: elem.type || '',
|
|
name: elem.name || '',
|
|
id: elem.id || '',
|
|
value: elem.value || '',
|
|
placeholder: elem.placeholder || '',
|
|
required: elem.required || false,
|
|
disabled: elem.disabled || false,
|
|
readonly: elem.readOnly || false
|
|
};
|
|
|
|
// For select elements, get options
|
|
if (elem.tagName.toLowerCase() === 'select') {
|
|
info.options = Array.from(elem.options).map(opt => ({
|
|
value: opt.value,
|
|
text: opt.text,
|
|
selected: opt.selected
|
|
}));
|
|
}
|
|
|
|
// For checkboxes and radios, get checked state
|
|
if (elem.type === 'checkbox' || elem.type === 'radio') {
|
|
info.checked = elem.checked || false;
|
|
}
|
|
|
|
elements.push(info);
|
|
});
|
|
|
|
return elements;
|
|
});
|
|
|
|
if (inputs.length > 0) {
|
|
console.log('Form Input Elements:');
|
|
for (const input of inputs) {
|
|
let desc = ` <${input.tag}`;
|
|
if (input.type) desc += ` type="${input.type}"`;
|
|
if (input.name) desc += ` name="${input.name}"`;
|
|
if (input.id) desc += ` id="${input.id}"`;
|
|
desc += '>';
|
|
console.log(desc);
|
|
|
|
if (input.value && input.type !== 'password') {
|
|
console.log(` Value: "${input.value}"`);
|
|
}
|
|
if (input.placeholder) {
|
|
console.log(` Placeholder: "${input.placeholder}"`);
|
|
}
|
|
if (input.required) {
|
|
console.log(` Required: true`);
|
|
}
|
|
if (input.disabled) {
|
|
console.log(` Disabled: true`);
|
|
}
|
|
if (input.readonly) {
|
|
console.log(` Readonly: true`);
|
|
}
|
|
if (input.checked !== undefined) {
|
|
console.log(` Checked: ${input.checked}`);
|
|
}
|
|
if (input.options) {
|
|
console.log(` Options:`);
|
|
for (const opt of input.options) {
|
|
console.log(` - "${opt.text}" (value: "${opt.value}")${opt.selected ? ' [selected]' : ''}`);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log('Form Input Elements: None found');
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Show cookies if --cookies flag is passed
|
|
if (options.cookies) {
|
|
const cookies = await context.cookies();
|
|
if (cookies.length > 0) {
|
|
console.log('Cookies:');
|
|
for (const cookie of cookies) {
|
|
console.log(` ${cookie.name}: ${cookie.value}`);
|
|
if (cookie.domain) console.log(` Domain: ${cookie.domain}`);
|
|
if (cookie.path) console.log(` Path: ${cookie.path}`);
|
|
if (cookie.expires) console.log(` Expires: ${new Date(cookie.expires * 1000).toISOString()}`);
|
|
if (cookie.httpOnly) console.log(` HttpOnly: true`);
|
|
if (cookie.secure) console.log(` Secure: true`);
|
|
if (cookie.sameSite) console.log(` SameSite: ${cookie.sameSite}`);
|
|
}
|
|
} else {
|
|
console.log('Cookies: None');
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Show storage if --storage flag is passed
|
|
if (options.storage) {
|
|
const storageData = await page.evaluate(() => {
|
|
const data = {
|
|
localStorage: {},
|
|
sessionStorage: {}
|
|
};
|
|
|
|
// Get localStorage
|
|
try {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
data.localStorage[key] = localStorage.getItem(key);
|
|
}
|
|
} catch (e) {
|
|
data.localStorage = 'Error accessing localStorage: ' + e.message;
|
|
}
|
|
|
|
// Get sessionStorage
|
|
try {
|
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
const key = sessionStorage.key(i);
|
|
data.sessionStorage[key] = sessionStorage.getItem(key);
|
|
}
|
|
} catch (e) {
|
|
data.sessionStorage = 'Error accessing sessionStorage: ' + e.message;
|
|
}
|
|
|
|
return data;
|
|
});
|
|
|
|
console.log('localStorage:');
|
|
if (typeof storageData.localStorage === 'string') {
|
|
console.log(` ${storageData.localStorage}`);
|
|
} else if (Object.keys(storageData.localStorage).length > 0) {
|
|
for (const [key, value] of Object.entries(storageData.localStorage)) {
|
|
console.log(` ${key}: ${value}`);
|
|
}
|
|
} else {
|
|
console.log(' (empty)');
|
|
}
|
|
console.log('');
|
|
|
|
console.log('sessionStorage:');
|
|
if (typeof storageData.sessionStorage === 'string') {
|
|
console.log(` ${storageData.sessionStorage}`);
|
|
} else if (Object.keys(storageData.sessionStorage).length > 0) {
|
|
for (const [key, value] of Object.entries(storageData.sessionStorage)) {
|
|
console.log(` ${key}: ${value}`);
|
|
}
|
|
} else {
|
|
console.log(' (empty)');
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Check if body contains rsx_dump_die() output
|
|
const has_rsx_dump_die = body && body.includes('rsx_dump_die() called');
|
|
|
|
// Show response body unless --no-body is passed (but always show rsx_dump_die output)
|
|
if (!options.no_body || has_rsx_dump_die) {
|
|
if (has_rsx_dump_die) {
|
|
// Extract and show only the rsx_dump_die() output when --no-body is set
|
|
if (options.no_body) {
|
|
const dump_start = body.indexOf('rsx_dump_die() called');
|
|
const dump_output = body.substring(dump_start);
|
|
console.log('rsx_dump_die() Output Detected:');
|
|
console.log(dump_output);
|
|
} else {
|
|
// Show full body when --no-body is not set
|
|
console.log('HTTP Response Body:');
|
|
console.log(body);
|
|
}
|
|
} else if (!options.no_body) {
|
|
// Normal body output when no rsx_dump_die
|
|
console.log('HTTP Response Body:');
|
|
if (body && body.trim()) {
|
|
console.log(body);
|
|
} else {
|
|
console.log('(empty)');
|
|
|
|
// If 500 with empty body, show Laravel log
|
|
if (status === 500 && fs.existsSync(laravel_log_path)) {
|
|
console.log('');
|
|
console.log('Laravel Log:');
|
|
try {
|
|
const log = fs.readFileSync(laravel_log_path, 'utf8');
|
|
// Get last 50 lines of log
|
|
const lines = log.trim().split('\n');
|
|
const lastLines = lines.slice(-50).join('\n');
|
|
console.log(lastLines);
|
|
} catch (e) {
|
|
console.log(`Error reading log: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show Laravel log if --log flag was passed
|
|
if (options.show_log && fs.existsSync(laravel_log_path)) {
|
|
console.log('');
|
|
console.log('Laravel Log:');
|
|
try {
|
|
const log = fs.readFileSync(laravel_log_path, 'utf8');
|
|
console.log(log);
|
|
} catch (e) {
|
|
console.log(`Error reading log: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Display logs based on --all-logs flag
|
|
console.log('');
|
|
|
|
const nginx_error_log = '/var/log/nginx/error.log';
|
|
const nginx_access_log = '/var/log/nginx/access.log';
|
|
|
|
if (options.all_logs) {
|
|
// Display all logs when --all-logs is passed
|
|
|
|
// Laravel log
|
|
console.log('\x1b[1m\x1b[37mlaravel.log:\x1b[0m');
|
|
if (fs.existsSync(laravel_log_path)) {
|
|
try {
|
|
const log = fs.readFileSync(laravel_log_path, 'utf8');
|
|
console.log(log || '(empty)');
|
|
} catch (e) {
|
|
console.log(`Error reading log: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log('(file not found)');
|
|
}
|
|
console.log('');
|
|
|
|
// Nginx error log
|
|
console.log('\x1b[1m\x1b[37mnginx error.log:\x1b[0m');
|
|
if (fs.existsSync(nginx_error_log)) {
|
|
try {
|
|
const log = fs.readFileSync(nginx_error_log, 'utf8');
|
|
console.log(log || '(empty)');
|
|
} catch (e) {
|
|
console.log(`Error reading log: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log('(file not found)');
|
|
}
|
|
console.log('');
|
|
|
|
// Nginx access log
|
|
console.log('\x1b[1m\x1b[37mnginx access.log:\x1b[0m');
|
|
if (fs.existsSync(nginx_access_log)) {
|
|
try {
|
|
const log = fs.readFileSync(nginx_access_log, 'utf8');
|
|
console.log(log || '(empty)');
|
|
} catch (e) {
|
|
console.log(`Error reading log: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log('(file not found)');
|
|
}
|
|
} else {
|
|
// Default: Only show errors/warnings
|
|
|
|
// Check Laravel log for errors/warnings
|
|
if (fs.existsSync(laravel_log_path)) {
|
|
try {
|
|
const log = fs.readFileSync(laravel_log_path, 'utf8');
|
|
const lines = log.split('\n');
|
|
const errorLines = lines.filter(line =>
|
|
line.includes('.ERROR') ||
|
|
line.includes('.WARNING') ||
|
|
line.includes('.CRITICAL') ||
|
|
line.includes('.ALERT') ||
|
|
line.includes('.EMERGENCY')
|
|
);
|
|
|
|
if (errorLines.length > 0) {
|
|
console.log('\x1b[1m\x1b[37mlaravel.log errors:\x1b[0m');
|
|
console.log(errorLines.join('\n'));
|
|
console.log('');
|
|
}
|
|
} catch (e) {
|
|
// Ignore read errors
|
|
}
|
|
}
|
|
|
|
// Check nginx error log
|
|
if (fs.existsSync(nginx_error_log)) {
|
|
try {
|
|
const log = fs.readFileSync(nginx_error_log, 'utf8');
|
|
if (log && log.trim()) {
|
|
console.log('\x1b[1m\x1b[37mnginx error.log:\x1b[0m');
|
|
console.log(log);
|
|
console.log('');
|
|
}
|
|
} catch (e) {
|
|
// Ignore read errors
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (status >= 400 || consoleErrors.length > 0 || networkFailures.length > 0) {
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
})(); |