Files
telegram-shop/tests/scripts/admin-visual-test.js
2026-07-07 18:48:40 +01:00

258 lines
9.7 KiB
JavaScript

const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const TARGET_URL = process.env.TARGET_URL || 'http://127.0.0.1:3001';
const TOKEN = process.env.ADMIN_TOKEN || 'shop_admin_2024_secure_token';
const REPORTS_DIR = process.env.REPORTS_DIR || './reports';
const OUTPUT_DIR = process.env.OUTPUT_DIR || './visual/admin-screenshots';
const PAGES = [
{ path: '/', name: 'dashboard' },
{ path: '/users', name: 'users' },
{ path: '/products', name: 'products' },
{ path: '/categories', name: 'categories' },
{ path: '/catalog', name: 'catalog' },
{ path: '/purchases', name: 'purchases' },
{ path: '/audit', name: 'audit' },
{ path: '/settings', name: 'settings' },
{ path: '/wallets', name: 'wallets' },
{ path: '/payment-wallets', name: 'payment-wallets' },
{ path: '/seed', name: 'seed' },
{ path: '/locales', name: 'locales' },
];
const VIEWPORTS = {
desktop: { width: 1920, height: 1080 },
tablet: { width: 768, height: 1024 },
mobile: { width: 375, height: 667 },
};
async function login(context) {
const page = await context.newPage();
const loginUrl = `${TARGET_URL}/login`;
console.log(`Logging in at ${loginUrl}`);
await page.goto(loginUrl, { waitUntil: 'networkidle', timeout: 30000 });
const tokenInput = await page.locator('input[name="token"], input#token, input[type="password"]').first();
if (await tokenInput.count() > 0) {
await tokenInput.fill(TOKEN);
}
const submitBtn = await page.locator('button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")').first();
if (await submitBtn.count() > 0) {
await submitBtn.click();
} else {
await page.evaluate((token) => {
const form = document.querySelector('form');
if (form) {
const input = form.querySelector('input[name="token"]') || form.querySelector('input[type="password"]');
if (input) input.value = token;
form.submit();
}
}, TOKEN);
}
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const cookies = await context.cookies();
console.log('Cookies after login:', cookies.map(c => c.name));
await page.close();
}
async function captureAndMonitor(context, pageInfo, viewportName, outputDir) {
const page = await context.newPage();
const fullUrl = `${TARGET_URL}${pageInfo.path}`;
const filename = `${pageInfo.name}_${viewportName}.png`;
const filepath = path.join(outputDir, filename);
const consoleErrors = [];
const networkErrors = [];
const assetErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push({ type: 'console_error', text: msg.text(), location: msg.location() });
}
});
page.on('pageerror', err => {
consoleErrors.push({ type: 'page_error', text: err.message, stack: err.stack });
});
page.on('requestfailed', req => {
networkErrors.push({ type: 'network_failure', url: req.url(), resourceType: req.resourceType(), error: req.failure()?.errorText });
});
page.on('response', res => {
if (res.status() >= 400) {
const req = res.request();
const entry = { type: 'http_error', url: res.url(), status: res.status(), statusText: res.statusText(), resourceType: req.resourceType() };
if (req.resourceType() === 'stylesheet' || req.resourceType() === 'script' || req.url().match(/\.(css|js|woff|woff2)$/)) {
assetErrors.push(entry);
}
networkErrors.push(entry);
}
});
await page.setViewportSize(VIEWPORTS[viewportName]);
try {
await page.goto(fullUrl, { waitUntil: 'networkidle', timeout: 30000 });
} catch (e) {
await page.goto(fullUrl, { waitUntil: 'load', timeout: 30000 });
}
await page.waitForTimeout(1500);
if (pageInfo.name === 'catalog') {
try {
const accordionHeaders = await page.locator('.accordion-button, [data-toggle="collapse"], .catalog-toggle').all();
for (const header of accordionHeaders.slice(0, 3)) {
await header.click().catch(() => {});
await page.waitForTimeout(300);
}
await page.waitForTimeout(500);
} catch (e) {}
}
if (pageInfo.name === 'users') {
try {
const collapseBtn = await page.locator('button:has-text("Adjust Balance"), [data-toggle="collapse"]:has-text("Adjust"), .adjust-balance-toggle').first();
if (await collapseBtn.count() > 0) {
await collapseBtn.click().catch(() => {});
await page.waitForTimeout(500);
}
} catch (e) {}
}
await page.screenshot({ path: filepath, fullPage: false });
await page.close();
return {
page: pageInfo.name,
path: pageInfo.path,
viewport: viewportName,
file: filename,
url: fullUrl,
consoleErrors,
networkErrors,
assetErrors,
status: networkErrors.some(e => e.status >= 500) ? 'ERROR' : 'OK',
};
}
async function main() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.mkdirSync(REPORTS_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
await login(context);
const results = [];
for (const pageInfo of PAGES) {
for (const vpName of Object.keys(VIEWPORTS)) {
try {
const result = await captureAndMonitor(context, pageInfo, vpName, OUTPUT_DIR);
results.push(result);
const errSummary = result.consoleErrors.length + result.networkErrors.length;
console.log(`[${result.status}] ${pageInfo.name} @ ${vpName} (${errSummary} errors) -> ${result.file}`);
} catch (err) {
results.push({ page: pageInfo.name, path: pageInfo.path, viewport: vpName, status: 'ERROR', error: err.message, consoleErrors: [], networkErrors: [], assetErrors: [] });
console.error(`[ERROR] ${pageInfo.name} @ ${vpName}: ${err.message}`);
}
}
}
await browser.close();
const summary = {
timestamp: new Date().toISOString(),
targetUrl: TARGET_URL,
totalScreenshots: results.filter(r => !r.error).length,
totalErrors: results.filter(r => r.status === 'ERROR' || r.error).length,
pages: PAGES.map(p => {
const pageResults = results.filter(r => r.page === p.name);
const anyError = pageResults.some(r => r.status === 'ERROR' || r.error || r.consoleErrors?.length > 0 || r.networkErrors?.length > 0);
return {
name: p.name,
path: p.path,
status: pageResults.some(r => r.status === 'ERROR') ? 'ERROR' : (anyError ? 'WARN' : 'OK'),
viewports: Object.fromEntries(pageResults.map(r => [r.viewport, { file: r.file, status: r.status, consoleErrors: r.consoleErrors?.length || 0, networkErrors: r.networkErrors?.length || 0 }])),
consoleErrors: pageResults.flatMap(r => r.consoleErrors || []),
networkErrors: pageResults.flatMap(r => r.networkErrors || []),
assetErrors: pageResults.flatMap(r => r.assetErrors || []),
};
}),
results,
};
const reportPath = path.join(REPORTS_DIR, 'admin-visual-test-report.json');
fs.writeFileSync(reportPath, JSON.stringify(summary, null, 2));
console.log(`\nReport saved to ${reportPath}`);
console.log('\n## Visual Test Results\n');
console.log('| Page | Status | Viewports | Console Errors | Network Errors | Notes |');
console.log('|------|--------|-----------|----------------|----------------|-------|');
for (const p of summary.pages) {
const vpStatus = Object.entries(p.viewports).map(([vp, data]) => `${vp}: ${data.status}`).join(', ');
const ce = p.consoleErrors.length;
const ne = p.networkErrors.length;
const notes = [];
if (p.assetErrors?.length > 0) notes.push(`${p.assetErrors.length} asset 404s`);
if (p.consoleErrors.some(e => e.type === 'page_error')) notes.push('page errors');
console.log(`| ${p.name} | ${p.status} | ${vpStatus} | ${ce} | ${ne} | ${notes.join('; ') || '-'} |`);
}
const mdPath = path.join(REPORTS_DIR, 'admin-visual-test-report.md');
let md = `## Visual Test Results for Admin Panel\n\n`;
md += `**URL:** ${TARGET_URL}\n`;
md += `**Date:** ${summary.timestamp}\n\n`;
md += `| Page | Status | Desktop | Tablet | Mobile | Console | Network | Notes |\n`;
md += `|------|--------|---------|--------|--------|---------|---------|-------|\n`;
for (const p of summary.pages) {
const d = p.viewports.desktop?.status || 'N/A';
const t = p.viewports.tablet?.status || 'N/A';
const m = p.viewports.mobile?.status || 'N/A';
const ce = (p.consoleErrors || []).length;
const ne = (p.networkErrors || []).length;
const notes = [];
if ((p.assetErrors || []).length > 0) notes.push(`${p.assetErrors.length} asset 404s`);
if (p.consoleErrors.some(e => e.type === 'page_error')) notes.push('JS page errors');
md += `| ${p.name} | ${p.status} | ${d} | ${t} | ${m} | ${ce} | ${ne} | ${notes.join('; ') || '-'} |\n`;
}
md += `\n### Screenshot Files\n\n`;
for (const p of summary.pages) {
for (const [vp, data] of Object.entries(p.viewports)) {
md += `- \`${data.file}\`\n`;
}
}
md += `\n### Asset 404 Errors\n\n`;
const allAssetErrors = summary.pages.flatMap(p => p.assetErrors || []);
if (allAssetErrors.length === 0) {
md += 'No asset 404 errors found.\n';
} else {
for (const e of allAssetErrors) {
md += `- \`${e.url}\` (${e.status} ${e.statusText})\n`;
}
}
md += `\n### Runtime JS Errors\n\n`;
const allPageErrors = summary.pages.flatMap(p => p.consoleErrors.filter(e => e.type === 'page_error') || []);
if (allPageErrors.length === 0) {
md += 'No runtime JS page errors found.\n';
} else {
for (const e of allPageErrors) {
md += `- \`${e.text}\`\n`;
}
}
fs.writeFileSync(mdPath, md);
console.log(`Markdown report saved to ${mdPath}`);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});