- Add tests/scripts/lib/gitea-client.js: Gitea API client with auth, comments,
attachments, and Markdown report formatters for visual and console reports
- Add tests/scripts/lib/browser-launcher.js: shared Playwright launch config with
--dns-resolution-order=hostname-first, realistic UA, and navigateTo() helper
using waitUntil:'commit' + waitForLoadState('domcontentloaded')
- Add tests/scripts/e2e-booking-flow-v2.js: full E2E scenario for irina-vik.ru
(register → book service → login → personal cabinet) with Gitea reporting
- Update visual-test-pipeline.js: GITEA_ISSUE env var, Gitea comment+attachment
posting, browser-launcher integration, waitUntil:'commit' navigation
- Update console-error-monitor-standalone.js: same Gitea + DNS fixes
- Update capture-screenshots.js: browser-launcher integration, DNS fix
- Update docker-compose.web-testing.yml: NETWORK_MODE env var (bridge),
DNS_RESOLUTION_ORDER, GITEA_USER/PASSWORD env passthrough, e2e-booking service
- Update tests/package.json: pin playwright to exact 1.52.0 (matches Docker image)
- Update .gitignore: add tests/visual/e2e/ for E2E screenshots
- Update .kilo/agents/visual-tester.md: Docker networking note, Gitea scripts,
e2e-booking service, updated script table
- Update .kilo/commands/web-test.md: Docker Networking section, --issue flag,
Gitea Integration section, e2e-booking service
- Update .kilo/commands/e2e-test.md: complete rewrite — Docker-based Playwright,
no more MCP dependency, proper service table, Gitea integration docs
- Update .kilo/capability-index.yaml: add gitea_integration, e2e_booking_flow,
docker_networking capabilities to visual-tester; add routing entries
263 lines
8.4 KiB
JavaScript
263 lines
8.4 KiB
JavaScript
/**
|
|
* Gitea API Client — Lightweight helper for posting test results to Gitea Issues.
|
|
*
|
|
* Auth flow: Basic Auth → create token → use token for API calls.
|
|
*
|
|
* Usage:
|
|
* const gitea = require('./lib/gitea-client');
|
|
* await gitea.postComment(issueNumber, body);
|
|
* await gitea.uploadAttachment(issueNumber, filePath);
|
|
*
|
|
* Environment:
|
|
* GITEA_API_URL - API base (default: https://git.softuniq.eu/api/v1)
|
|
* GITEA_TOKEN - Pre-existing API token (skips Basic Auth if set)
|
|
* GITEA_USER - Username for Basic Auth (default: NW)
|
|
* GITEA_PASSWORD - Password for Basic Auth (required if no token)
|
|
* GITEA_REPO - Repository path (default: UniqueSoft/APAW)
|
|
*/
|
|
|
|
const https = require('https');
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const GITEA_API_URL = process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1';
|
|
const GITEA_USER = process.env.GITEA_USER || '';
|
|
const GITEA_PASSWORD = process.env.GITEA_PASSWORD || '';
|
|
const GITEA_REPO = process.env.GITEA_REPO || 'UniqueSoft/APAW';
|
|
|
|
let _cachedToken = process.env.GITEA_TOKEN || null;
|
|
|
|
function request(urlStr, options, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(urlStr);
|
|
const mod = url.protocol === 'https:' ? https : http;
|
|
const opts = {
|
|
hostname: url.hostname,
|
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
path: url.pathname + url.search,
|
|
method: options.method || 'GET',
|
|
headers: options.headers || {},
|
|
};
|
|
const req = mod.request(opts, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
|
} else {
|
|
reject(new Error(`Gitea API ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function getToken() {
|
|
if (_cachedToken) return _cachedToken;
|
|
|
|
const credentials = Buffer.from(`${GITEA_USER}:${GITEA_PASSWORD}`).toString('base64');
|
|
const urlStr = `${GITEA_API_URL}/users/${GITEA_USER}/tokens`;
|
|
const body = JSON.stringify({ name: `vt-${Date.now()}`, scopes: ['all'] });
|
|
|
|
const result = await request(urlStr, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Basic ${credentials}`,
|
|
},
|
|
}, body);
|
|
|
|
_cachedToken = result.sha1;
|
|
return _cachedToken;
|
|
}
|
|
|
|
async function authHeaders() {
|
|
const token = await getToken();
|
|
return { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
async function postComment(issueNumber, body) {
|
|
const headers = await authHeaders();
|
|
const url = `${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/comments`;
|
|
return request(url, { method: 'POST', headers }, JSON.stringify({ body }));
|
|
}
|
|
|
|
async function uploadAttachment(issueNumber, filePath) {
|
|
const token = await getToken();
|
|
const fileContent = fs.readFileSync(filePath);
|
|
const filename = path.basename(filePath);
|
|
const boundary = `----FormBoundary${Date.now()}`;
|
|
|
|
let body = `--${boundary}\r\n`.getBytes?.() || Buffer.from(`--${boundary}\r\n`);
|
|
body = Buffer.concat([
|
|
Buffer.from(`--${boundary}\r\n`),
|
|
Buffer.from(`Content-Disposition: form-data; name="attachment"; filename="${filename}"\r\n`),
|
|
Buffer.from(`Content-Type: image/png\r\n\r\n`),
|
|
fileContent,
|
|
Buffer.from(`\r\n--${boundary}--\r\n`),
|
|
]);
|
|
|
|
const url = new URL(`${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/assets`);
|
|
const mod = url.protocol === 'https:' ? https : http;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const req = mod.request({
|
|
hostname: url.hostname,
|
|
port: url.port || 443,
|
|
path: url.pathname,
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `token ${token}`,
|
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
'Content-Length': body.length,
|
|
},
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
|
} else {
|
|
reject(new Error(`Gitea upload ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function uploadAndComment(issueNumber, filePaths, commentBody) {
|
|
const uuids = [];
|
|
for (const fp of filePaths) {
|
|
try {
|
|
const result = await uploadAttachment(issueNumber, fp);
|
|
uuids.push({ filename: path.basename(fp), uuid: result.uuid });
|
|
} catch (err) {
|
|
console.error(` ⚠️ Upload failed ${path.basename(fp)}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
let fullBody = commentBody;
|
|
if (uuids.length > 0) {
|
|
fullBody += '\n\n### 📸 Screenshots\n\n';
|
|
for (const u of uuids) {
|
|
fullBody += `\n`;
|
|
}
|
|
}
|
|
|
|
return postComment(issueNumber, fullBody);
|
|
}
|
|
|
|
function formatVisualReport(report) {
|
|
const s = report.summary;
|
|
const lines = [
|
|
'## 📊 Visual Test Results',
|
|
'',
|
|
`**URL**: \`${report.targetUrl}\``,
|
|
`**Pages**: ${report.pages.join(', ')}`,
|
|
`**Viewports**: ${report.viewports.join(', ')}`,
|
|
`**Threshold**: ${report.threshold * 100}%`,
|
|
'',
|
|
'### Summary',
|
|
'',
|
|
`| Metric | Count |`,
|
|
`|--------|-------|`,
|
|
`| Screenshots captured | ${s.screenshotsCaptured} |`,
|
|
`| Screenshots failed | ${s.screenshotsFailed} |`,
|
|
`| Comparisons passed | ${s.comparisonsPassed} |`,
|
|
`| Comparisons failed | ${s.comparisonsFailed} |`,
|
|
`| UI elements extracted | ${s.totalElements} |`,
|
|
`| Console errors | ${s.totalConsoleErrors} |`,
|
|
`| Network errors | ${s.totalNetworkErrors} |`,
|
|
'',
|
|
`**Overall**: ${s.overallPassed ? '✅ PASSED' : '❌ FAILED'}`,
|
|
];
|
|
|
|
if (report.comparison?.length) {
|
|
lines.push('', '### Comparison Details', '');
|
|
lines.push('| Screenshot | Status | Diff % |');
|
|
lines.push('|------------|--------|--------|');
|
|
for (const c of report.comparison) {
|
|
lines.push(`| ${c.filename} | ${c.status === 'PASS' ? '✅' : '❌'} ${c.status} | ${c.diffPercent || 'N/A'} |`);
|
|
}
|
|
}
|
|
|
|
if (report.consoleErrors?.length > 0) {
|
|
lines.push('', '### Console Errors', '');
|
|
for (const e of report.consoleErrors.slice(0, 5)) {
|
|
lines.push(`- [${e.page}/${e.viewport}] ${e.error?.slice(0, 120)}`);
|
|
}
|
|
if (report.consoleErrors.length > 5) {
|
|
lines.push(`- ... and ${report.consoleErrors.length - 5} more`);
|
|
}
|
|
}
|
|
|
|
if (report.networkErrors?.length > 0) {
|
|
lines.push('', '### Network Errors', '');
|
|
for (const e of report.networkErrors.slice(0, 5)) {
|
|
lines.push(`- [${e.page}/${e.viewport}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
|
|
}
|
|
if (report.networkErrors.length > 5) {
|
|
lines.push(`- ... and ${report.networkErrors.length - 5} more`);
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function formatConsoleReport(report) {
|
|
const s = report.summary;
|
|
const lines = [
|
|
'## 📊 Console Error Monitor Results',
|
|
'',
|
|
`**URL**: \`${report.targetUrl}\``,
|
|
`**Pages**: ${report.pages.join(', ')}`,
|
|
'',
|
|
'### Summary',
|
|
'',
|
|
`| Metric | Count |`,
|
|
`|--------|-------|`,
|
|
`| Console errors | ${s.consoleErrors} |`,
|
|
`| Console warnings | ${s.consoleWarnings} |`,
|
|
`| Network errors | ${s.networkErrors} |`,
|
|
`| **Total issues** | **${s.totalIssues}** |`,
|
|
'',
|
|
`**Status**: ${s.totalIssues === 0 ? '✅ CLEAN' : '❌ ISSUES FOUND'}`,
|
|
];
|
|
|
|
if (report.consoleErrors?.length > 0) {
|
|
lines.push('', '### Console Errors', '');
|
|
for (const e of report.consoleErrors.slice(0, 8)) {
|
|
lines.push(`- [${e.page}] ${e.text?.slice(0, 120)}`);
|
|
}
|
|
if (report.consoleErrors.length > 8) {
|
|
lines.push(`- ... and ${report.consoleErrors.length - 8} more`);
|
|
}
|
|
}
|
|
|
|
if (report.networkErrors?.length > 0) {
|
|
lines.push('', '### Network Errors', '');
|
|
for (const e of report.networkErrors.slice(0, 8)) {
|
|
lines.push(`- [${e.page}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
|
|
}
|
|
if (report.networkErrors.length > 8) {
|
|
lines.push(`- ... and ${report.networkErrors.length - 8} more`);
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
module.exports = {
|
|
postComment,
|
|
uploadAttachment,
|
|
uploadAndComment,
|
|
formatVisualReport,
|
|
formatConsoleReport,
|
|
}; |