- 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
352 lines
9.1 KiB
JavaScript
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);
|
|
}); |