Files
APAW/tests/scripts/console-error-monitor.js
¨NW¨ e074612046 feat: add web testing infrastructure
- Docker configurations for Playwright MCP (no host pollution)
- Visual regression testing with pixelmatch
- Link checking for 404/500 errors
- Console error detection with Gitea issue creation
- Form testing capabilities
- /web-test and /web-test-fix commands
- web-testing skill documentation
- Reorganize project structure (docker/, scripts/, tests/)
- Update orchestrator model to ollama-cloud/glm-5

Structure:
- docker/ - Docker configurations (moved from archive)
- scripts/ - Utility scripts
- tests/ - Test suite with visual, console, links testing
- .kilo/commands/ - /web-test and /web-test-fix commands
- .kilo/skills/ - web-testing skill

Issues: #58 #60 #62
2026-04-07 08:55:24 +01:00

352 lines
9.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Console Error Aggregator
*
* Collects all console errors from Playwright sessions
* Reports: error message, file, line number, stack trace
* Auto-creates Gitea Issues for critical errors
*/
const http = require('http');
const https = require('https');
const { URL } = require('url');
// Configuration
const config = {
playwrightMcpUrl: process.env.PLAYWRIGHT_MCP_URL || 'http://localhost:8931/mcp',
giteaApiUrl: process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1',
giteaToken: process.env.GITEA_TOKEN || '',
giteaRepo: process.env.GITEA_REPO || 'UniqueSoft/APAW',
targetUrl: process.env.TARGET_URL || 'http://localhost:3000',
reportsDir: process.env.REPORTS_DIR || './reports',
autoCreateIssues: process.env.AUTO_CREATE_ISSUES === 'true',
ignoredPatterns: (process.env.IGNORED_ERROR_PATTERNS || '').split(','),
};
/**
* Make HTTP request to Playwright MCP
*/
async function mcpRequest(method, params) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method,
params,
});
const url = new URL(config.playwrightMcpUrl);
const req = http.request({
hostname: url.hostname,
port: url.port || 8931,
path: '/mcp',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', reject);
req.write(body);
req.end();
});
}
/**
* Navigate to URL
*/
async function navigateTo(url) {
return mcpRequest('tools/call', {
name: 'browser_navigate',
arguments: { url },
});
}
/**
* Get console messages
*/
async function getConsoleMessages(level = 'error', all = true) {
return mcpRequest('tools/call', {
name: 'browser_console_messages',
arguments: { level, all },
});
}
/**
* Get network requests (for failed requests)
*/
async function getNetworkRequests(filter = 'failed') {
return mcpRequest('tools/call', {
name: 'browser_network_requests',
arguments: { filter },
});
}
/**
* Take screenshot for error context
*/
async function takeScreenshot(filename) {
return mcpRequest('tools/call', {
name: 'browser_take_screenshot',
arguments: { filename },
});
}
/**
* Parse console error to extract file and line number
*/
function parseErrorDetails(error) {
const result = {
message: error,
file: null,
line: null,
column: null,
stack: [],
};
// Try to parse stack trace
const stackMatch = error.match(/at\s+(?:(.+)\s+\()?([^:]+):(\d+):(\d+)\)?/);
if (stackMatch) {
result.file = stackMatch[2];
result.line = parseInt(stackMatch[3]);
result.column = parseInt(stackMatch[4]);
}
// Parse Chrome-style stack traces
const chromePattern = /at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/g;
let match;
while ((match = chromePattern.exec(error)) !== null) {
result.stack.push({
function: match[1],
file: match[2],
line: parseInt(match[3]),
column: parseInt(match[4]),
});
}
return result;
}
/**
* Check if error should be ignored
*/
function shouldIgnoreError(error) {
const message = error.message || error;
return config.ignoredPatterns.some(pattern =>
pattern && message.includes(pattern)
);
}
/**
* Create Gitea Issue for error
*/
async function createGiteaIssue(errorData) {
if (!config.giteaToken || !config.autoCreateIssues) {
return null;
}
const fs = require('fs');
const path = require('path');
const title = `[Console Error] ${errorData.parsed.message.slice(0, 100)}`;
const body = `## Console Error
**Error Type**: ${errorData.type}
**Message**:
\`\`\`
${errorData.parsed.message}
\`\`\`
**Location**: ${errorData.parsed.file || 'Unknown'}:${errorData.parsed.line || '?'}
**Page URL**: ${errorData.pageUrl}
### Stack Trace
\`\`\`
${errorData.parsed.stack.map(s => `${s.function} (${s.file}:${s.line}:${s.column})`).join('\n') || 'No stack trace available'}
\`\`\`
## Auto-Fix Required
- [ ] Investigate the root cause
- [ ] Implement fix
- [ ] Add test case
- [ ] Verify fix
---
**Detected by**: Kilo Code Web Testing
`;
return new Promise((resolve, reject) => {
const url = new URL(`${config.giteaApiUrl}/repos/${config.giteaRepo}/issues`);
const bodyData = JSON.stringify({ title, body });
const client = url.protocol === 'https:' ? https : http;
const req = client.request({
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `token ${config.giteaToken}`,
'Content-Length': Buffer.byteLength(bodyData),
},
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.write(bodyData);
req.end();
});
}
/**
* Main console monitoring function
*/
async function main() {
console.log('=== Console Error Monitor ===\n');
console.log(`Target URL: ${config.targetUrl}`);
console.log(`Auto-create Issues: ${config.autoCreateIssues}\n`);
const errors = {
consoleErrors: [],
networkErrors: [],
uncaughtExceptions: [],
};
try {
// Navigate to target
console.log('📡 Navigating to target URL...');
await navigateTo(config.targetUrl);
// Wait a bit for page to load
await new Promise(resolve => setTimeout(resolve, 2000));
// Get console messages
console.log('🔍 Collecting console messages...');
const consoleResult = await getConsoleMessages('error', true);
if (consoleResult.result?.content) {
const messages = consoleResult.result.content;
for (const msg of messages) {
if (shouldIgnoreError(msg)) {
console.log(' ⏭️ Ignored:', msg.slice(0, 80));
continue;
}
const parsed = parseErrorDetails(msg);
const errorData = {
type: 'console',
message: msg,
parsed,
pageUrl: config.targetUrl,
timestamp: new Date().toISOString(),
};
errors.consoleErrors.push(errorData);
console.log(' ❌ Console Error:', msg.slice(0, 80));
}
}
// Get failed network requests
console.log('🔍 Checking network requests...');
const networkResult = await getNetworkRequests('failed');
if (networkResult.result?.content) {
for (const req of networkResult.result.content) {
if (req.status >= 400) {
errors.networkErrors.push({
type: 'network',
url: req.url,
status: req.status,
method: req.method,
pageUrl: config.targetUrl,
timestamp: new Date().toISOString(),
});
console.log(` ❌ Network Error: ${req.status} ${req.url}`);
}
}
}
// Take screenshot for context
const screenshotFilename = `error-context-${Date.now()}.png`;
await takeScreenshot(screenshotFilename);
console.log(`📸 Screenshot saved: ${screenshotFilename}`);
// Create Gitea Issues for critical errors
if (config.autoCreateIssues) {
console.log('\n📝 Creating Gitea Issues...');
for (const error of errors.consoleErrors) {
try {
const issue = await createGiteaIssue(error);
error.giteaIssue = issue?.html_url || null;
if (issue) {
console.log(` ✅ Issue created: ${issue.html_url}`);
error.issueNumber = issue.number;
}
} catch (err) {
console.log(` ❌ Failed to create issue: ${err.message}`);
}
}
}
} catch (error) {
console.error('Error during monitoring:', error.message);
}
// Generate report
const fs = require('fs');
const path = require('path');
const report = {
timestamp: new Date().toISOString(),
config: {
targetUrl: config.targetUrl,
autoCreateIssues: config.autoCreateIssues,
},
summary: {
consoleErrors: errors.consoleErrors.length,
networkErrors: errors.networkErrors.length,
totalErrors: errors.consoleErrors.length + errors.networkErrors.length,
},
errors,
};
const reportPath = path.join(config.reportsDir, 'console-errors-report.json');
if (!fs.existsSync(config.reportsDir)) {
fs.mkdirSync(config.reportsDir, { recursive: true });
}
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log('\n📊 Summary:');
console.log(` Console Errors: ${errors.consoleErrors.length}`);
console.log(` Network Errors: ${errors.networkErrors.length}`);
console.log(` Total Errors: ${report.summary.totalErrors}`);
console.log(`\n📄 Report saved to: ${reportPath}`);
// Exit with error if errors found
process.exit(report.summary.totalErrors > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});