Puppeteer-based screenshot tool with JWT auth, base64/file output modes, CSS animation disabling, and 1920x1080 viewport for ERP frontend captures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.7 KiB
JavaScript
146 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Screenshot API tool for visual validation from claude.ai
|
|
// Usage: NODE_PATH=/usr/lib/node_modules node /opt/erp/screenshot-api.js <url> [base64|file]
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const LOGIN_URL = 'http://localhost:5173/login';
|
|
const LOGIN_EMAIL = 'louis@metallcart.ru';
|
|
const LOGIN_PASSWORD = 'MetallKart2026!';
|
|
const API_LOGIN_URL = 'http://localhost:3001/api/v1/auth/login';
|
|
const VIEWPORT = { width: 1920, height: 1080 };
|
|
const GLOBAL_TIMEOUT = 30000;
|
|
|
|
const log = (...args) => console.error('[screenshot-api]', ...args);
|
|
|
|
async function main() {
|
|
const url = process.argv[2];
|
|
const outputFormat = process.argv[3] || 'base64';
|
|
|
|
if (!url) {
|
|
log('Usage: node screenshot-api.js <url> [base64|file]');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!['base64', 'file'].includes(outputFormat)) {
|
|
log('Invalid output format. Use "base64" or "file".');
|
|
process.exit(1);
|
|
}
|
|
|
|
let browser;
|
|
try {
|
|
browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-gpu',
|
|
],
|
|
timeout: GLOBAL_TIMEOUT,
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
await page.setViewport(VIEWPORT);
|
|
|
|
// Disable animations for stable screenshots
|
|
await page.evaluateOnNewDocument(() => {
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
*, *::before, *::after {
|
|
animation-duration: 0s !important;
|
|
animation-delay: 0s !important;
|
|
transition-duration: 0s !important;
|
|
transition-delay: 0s !important;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
});
|
|
|
|
// Login via API to get JWT tokens
|
|
log('Logging in via API...');
|
|
const http = require('http');
|
|
const tokens = await new Promise((resolve, reject) => {
|
|
const body = JSON.stringify({ email: LOGIN_EMAIL, password: LOGIN_PASSWORD });
|
|
const req = http.request(API_LOGIN_URL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
timeout: 10000,
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`Login failed: ${res.statusCode} ${data}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(new Error(`Login response parse error: ${data}`));
|
|
}
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('Login request timeout')); });
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
|
|
log('Login successful, injecting tokens...');
|
|
|
|
// Navigate to the app first to set localStorage on the correct origin
|
|
await page.goto('http://localhost:5173', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
|
|
// Inject tokens into localStorage
|
|
await page.evaluate((t) => {
|
|
localStorage.setItem('accessToken', t.accessToken);
|
|
localStorage.setItem('refreshToken', t.refreshToken);
|
|
if (t.user) localStorage.setItem('user', JSON.stringify(t.user));
|
|
}, tokens);
|
|
|
|
// Navigate to the target URL
|
|
log(`Navigating to ${url}...`);
|
|
try {
|
|
await page.goto(url, { waitUntil: 'networkidle0', timeout: 20000 });
|
|
} catch (e) {
|
|
log(`networkidle0 timeout, falling back to load...`);
|
|
await page.goto(url, { waitUntil: 'load', timeout: 15000 });
|
|
}
|
|
|
|
// Wait a bit for Ant Design components to finish rendering
|
|
await new Promise(r => setTimeout(r, 800));
|
|
|
|
// Take screenshot
|
|
const screenshotBuffer = await page.screenshot({ fullPage: false, type: 'png' });
|
|
|
|
if (outputFormat === 'base64') {
|
|
// Output ONLY base64 to stdout
|
|
process.stdout.write(screenshotBuffer.toString('base64'));
|
|
} else {
|
|
// Save to file
|
|
const screenshotsDir = '/opt/erp/screenshots';
|
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
const urlSlug = url.replace(/https?:\/\//, '').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 60);
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
const filename = `${urlSlug}_${timestamp}.png`;
|
|
const filepath = path.join(screenshotsDir, filename);
|
|
fs.writeFileSync(filepath, screenshotBuffer);
|
|
process.stdout.write(filepath);
|
|
}
|
|
|
|
log('Screenshot captured successfully.');
|
|
} catch (err) {
|
|
log('Error:', err.message);
|
|
process.exit(1);
|
|
} finally {
|
|
if (browser) {
|
|
await browser.close().catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
main();
|