Files
jabali-panel/tests/take-docs-screenshots.cjs
2026-02-06 02:47:43 +00:00

247 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate documentation screenshots for every admin/user route and each tab.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const puppeteer = require('puppeteer');
const args = process.argv.slice(2);
const getArg = (key, fallback) => {
const arg = args.find((item) => item.startsWith(`--${key}=`));
return arg ? arg.split('=').slice(1).join('=') : fallback;
};
const hasFlag = (flag) => args.includes(`--${flag}`);
const CONFIG = {
baseUrl: getArg('base-url', 'https://jabali.lan'),
outputDir: getArg('output-dir', '/var/www/jabali/docs/screenshots/full'),
pagesJson: getArg('pages-json', '/var/www/jabali/tests/docs-pages.json'),
admin: {
email: getArg('admin-email', 'admin@jabali.lan'),
password: getArg('admin-password', 'q1w2E#R$'),
path: getArg('admin-path', '/jabali-admin'),
},
user: {
path: getArg('user-path', '/jabali-panel'),
},
impersonateUserId: getArg('impersonate-user-id', '9'),
skipAdmin: hasFlag('skip-admin'),
skipUser: hasFlag('skip-user'),
fullPage: hasFlag('full-page'),
onlyMissing: hasFlag('only-missing'),
};
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function slugify(value) {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function loadPages() {
return JSON.parse(fs.readFileSync(CONFIG.pagesJson, 'utf-8'));
}
function queryId(table) {
try {
const out = execSync(
`sqlite3 /var/www/jabali/database/database.sqlite "select id from ${table} limit 1;"`,
{ encoding: 'utf-8' }
).trim();
return out || null;
} catch (err) {
return null;
}
}
function applyRecordIds(uri) {
if (!uri.includes('{record}')) return uri;
const resourceMap = {
'jabali-admin/users': 'users',
'jabali-admin/hosting-packages': 'hosting_packages',
'jabali-admin/geo-block-rules': 'geo_block_rules',
'jabali-admin/webhook-endpoints': 'webhook_endpoints',
};
const base = uri.split('/{record}')[0];
const table = resourceMap[base];
if (!table) return uri;
const id = queryId(table);
if (!id) return uri;
return uri.replace('{record}', id);
}
async function loginPanel(page, panelName, basePath, credentials) {
console.log(`Logging in to ${panelName} panel...`);
await page.goto(`${CONFIG.baseUrl}${basePath}/login`, { waitUntil: 'networkidle0' });
await wait(1500);
await page.type('input[type="email"]', credentials.email);
await page.type('input[type="password"]', credentials.password);
await page.click('button[type="submit"]');
await wait(4000);
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
throw new Error(`${panelName} login failed - still on login page`);
}
console.log(`${panelName} login successful!\n`);
}
async function loginUserViaImpersonation(page) {
if (!CONFIG.impersonateUserId) {
throw new Error('Missing --impersonate-user-id for user screenshots');
}
await loginPanel(page, 'Admin', CONFIG.admin.path, CONFIG.admin);
const impersonateUrl = `${CONFIG.baseUrl}/impersonate/start/${CONFIG.impersonateUserId}`;
console.log(`Impersonating user via: ${impersonateUrl}`);
await page.goto(impersonateUrl, { waitUntil: 'networkidle0' });
await wait(2000);
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
throw new Error('Impersonation failed - still on login page');
}
console.log('User impersonation successful!\n');
}
async function clickTab(page, tabLabel) {
const normalized = tabLabel.trim().toLowerCase();
const clicked = await page.evaluate((label) => {
const tabs = Array.from(document.querySelectorAll('[role="tab"], button'));
const match = tabs.find((el) => {
const text = (el.textContent || '').trim().toLowerCase();
return text === label;
}) || tabs.find((el) => {
const text = (el.textContent || '').trim().toLowerCase();
return text.includes(label);
});
if (match) {
match.click();
return true;
}
return false;
}, normalized);
if (clicked) {
await wait(1500);
}
return clicked;
}
async function captureRoute(context, entry, prefix) {
const page = await context.newPage();
await page.setViewport({ width: 1400, height: 900 });
const uri = applyRecordIds(entry.uri);
const url = `${CONFIG.baseUrl}/${uri}`.replace(/\/+$/, '');
const baseName = `${prefix}-${slugify(uri.replace(prefix === 'admin' ? 'jabali-admin' : 'jabali-panel', '')) || 'home'}`;
const filename = `${baseName}.png`;
const filepath = path.join(CONFIG.outputDir, filename);
const skipBase = CONFIG.onlyMissing && fs.existsSync(filepath);
try {
await page.goto(url, { waitUntil: 'networkidle0', timeout: 45000 });
await wait(2000);
if (!skipBase) {
await page.screenshot({ path: filepath, fullPage: CONFIG.fullPage });
console.log(`Saved: ${filepath}`);
}
if (entry.tabs && entry.tabs.length) {
for (const tab of entry.tabs) {
const tabFile = `${baseName}--tab-${slugify(tab)}.png`;
const tabPath = path.join(CONFIG.outputDir, tabFile);
if (CONFIG.onlyMissing && fs.existsSync(tabPath)) {
continue;
}
const clicked = await clickTab(page, tab);
if (!clicked) {
console.log(` Tab not found: ${tab}`);
continue;
}
await page.screenshot({ path: tabPath, fullPage: CONFIG.fullPage });
console.log(` Tab saved: ${tabPath}`);
}
}
} catch (err) {
console.log(`Error capturing ${url}: ${err.message}`);
} finally {
await page.close();
}
}
async function captureAuthPages() {
const pages = loadPages();
const authUris = pages.filter((entry) => entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor'));
if (!authUris.length) return;
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
});
const context = await browser.createBrowserContext();
for (const entry of authUris) {
const prefix = entry.panel === 'admin' ? 'admin' : 'user';
await captureRoute(context, entry, prefix);
}
await browser.close();
}
async function main() {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
const pages = loadPages();
await captureAuthPages();
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
});
if (!CONFIG.skipAdmin) {
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.setViewport({ width: 1400, height: 900 });
await loginPanel(page, 'Admin', CONFIG.admin.path, CONFIG.admin);
await page.close();
for (const entry of pages.filter((e) => e.panel === 'admin')) {
if (entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor')) {
continue;
}
await captureRoute(context, entry, 'admin');
}
await context.close();
}
if (!CONFIG.skipUser) {
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.setViewport({ width: 1400, height: 900 });
await loginUserViaImpersonation(page);
await page.close();
for (const entry of pages.filter((e) => e.panel === 'jabali')) {
if (entry.uri.includes('login') || entry.uri.includes('password-reset') || entry.uri.includes('two-factor')) {
continue;
}
await captureRoute(context, entry, 'user');
}
await context.close();
}
await browser.close();
console.log('Documentation screenshots completed.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});