| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import { spawn } from 'node:child_process';
- import { mkdir, writeFile } from 'node:fs/promises';
- import { existsSync, readFileSync } from 'node:fs';
- import { dirname, join, resolve } from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { chromium } from 'playwright';
- const __dirname = dirname(fileURLToPath(import.meta.url));
- const repoRoot = resolve(__dirname, '..');
- const apiBase = process.env.MM_API_BASE || resolveApiBase();
- const uiBase = process.env.MM_UI_BASE || 'http://127.0.0.1:15173';
- const username = process.env.MM_USERNAME || `codex_shots_${Date.now().toString(36)}`;
- const password = process.env.MM_PASSWORD || 'Codex123456';
- const skipStart = process.env.MM_SKIP_START === '1';
- const pages = [
- { name: 'dashboard', path: '/#/' },
- { name: 'accounts', path: '/#/accounts' },
- { name: 'works', path: '/#/works' },
- { name: 'publish', path: '/#/publish' },
- { name: 'analytics-overview', path: '/#/analytics/overview' },
- { name: 'analytics-platform', path: '/#/analytics/platform' },
- { name: 'analytics-account', path: '/#/analytics/account' },
- { name: 'analytics-work', path: '/#/analytics/work' },
- ];
- function resolveApiBase() {
- try {
- const envText = readFileSync(resolve(repoRoot, 'server', '.env'), 'utf8');
- const portMatch = envText.match(/^PORT=(.+)$/m);
- const hostMatch = envText.match(/^HOST=(.+)$/m);
- const port = portMatch?.[1]?.trim() || '3000';
- const rawHost = hostMatch?.[1]?.trim() || '127.0.0.1';
- const host = rawHost === '0.0.0.0' ? '127.0.0.1' : rawHost;
- return `http://${host}:${port}`;
- } catch {
- return 'http://127.0.0.1:3000';
- }
- }
- function getTimestamp() {
- return new Date().toISOString().replace(/[:.]/g, '-');
- }
- function createOutputDir() {
- const explicitDir = process.argv[2];
- return explicitDir
- ? resolve(repoRoot, explicitDir)
- : resolve(repoRoot, 'minimax-output', 'regression', getTimestamp());
- }
- function startProcess(label, args) {
- const child = spawnPnpm(args, {
- cwd: repoRoot,
- stdio: ['ignore', 'pipe', 'pipe'],
- windowsHide: true,
- });
- let logs = '';
- const appendLog = (chunk) => {
- logs += chunk.toString();
- if (logs.length > 6000) {
- logs = logs.slice(-6000);
- }
- };
- child.stdout.on('data', appendLog);
- child.stderr.on('data', appendLog);
- child.on('exit', (code) => {
- if (code && code !== 0) {
- console.error(`[${label}] exited with code ${code}`);
- console.error(logs);
- }
- });
- return { label, child, getLogs: () => logs };
- }
- async function stopProcess(proc) {
- if (!proc || proc.child.killed) return;
- proc.child.kill('SIGTERM');
- await new Promise((resolveStop) => setTimeout(resolveStop, 1500));
- if (!proc.child.killed && process.platform === 'win32') {
- spawn('taskkill', ['/PID', String(proc.child.pid), '/T', '/F'], {
- stdio: 'ignore',
- windowsHide: true,
- });
- }
- }
- async function waitForUrl(url, timeoutMs = 60000) {
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const response = await fetch(url);
- if (response.ok) return;
- } catch {
- // retry
- }
- await new Promise((resolveWait) => setTimeout(resolveWait, 1000));
- }
- throw new Error(`Timed out waiting for ${url}`);
- }
- async function ensurePlaywrightBrowser() {
- if (existsSync(chromium.executablePath())) return;
- console.log('[shots] Installing Playwright Chromium');
- await new Promise((resolveInstall, rejectInstall) => {
- const installer = spawnPnpm(['exec', 'playwright', 'install', 'chromium'], {
- cwd: repoRoot,
- stdio: 'inherit',
- windowsHide: true,
- });
- installer.on('exit', (code) => {
- if (code === 0) resolveInstall();
- else rejectInstall(new Error(`playwright install exited with code ${code}`));
- });
- });
- }
- function spawnPnpm(args, options) {
- if (process.platform === 'win32') {
- return spawn('cmd.exe', ['/d', '/s', '/c', 'pnpm', ...args], options);
- }
- return spawn('pnpm', args, options);
- }
- async function login(page) {
- await page.addInitScript((serverUrl) => {
- localStorage.setItem(
- 'server_configs',
- JSON.stringify([{ id: 'server_default', name: '本地 Node 服务', url: serverUrl, isDefault: true }]),
- );
- localStorage.setItem('current_server', 'server_default');
- }, apiBase);
- await page.goto(`${uiBase}/#/login`, { waitUntil: 'domcontentloaded' });
- await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
- await page.locator('.login-form input').nth(0).fill(username);
- await page.locator('.login-form input').nth(1).fill(password);
- await Promise.all([
- page.waitForURL(/#\/$/, { timeout: 30000 }),
- page.locator('.login-btn').click(),
- ]);
- await page.locator('.main-layout, .dashboard-page').first().waitFor({ timeout: 30000 });
- }
- async function capturePages(outputDir) {
- const browser = await chromium.launch({ headless: true });
- const context = await browser.newContext({
- viewport: { width: 1600, height: 1200 },
- deviceScaleFactor: 1,
- });
- const page = await context.newPage();
- await login(page);
- const manifest = [];
- for (const item of pages) {
- const targetPath = join(outputDir, `${item.name}.png`);
- console.log(`[shots] Capturing ${item.name}`);
- await page.goto(`${uiBase}${item.path}`, { waitUntil: 'domcontentloaded' });
- await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
- await page.waitForTimeout(1800);
- await page.screenshot({ path: targetPath, fullPage: true });
- manifest.push({ page: item.name, path: targetPath });
- }
- await writeFile(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
- await browser.close();
- }
- async function ensureRegressionUser() {
- if (process.env.MM_USERNAME && process.env.MM_PASSWORD) return;
- const payload = {
- username,
- password,
- email: `${username}@example.com`,
- };
- const response = await fetch(`${apiBase}/api/auth/register`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload),
- });
- if (response.ok) return;
- const text = await response.text();
- if (!text.includes('已存在') && !text.includes('exists')) {
- throw new Error(`Failed to create regression user: ${text}`);
- }
- }
- async function main() {
- const outputDir = createOutputDir();
- await mkdir(outputDir, { recursive: true });
- await ensurePlaywrightBrowser();
- let serverProc;
- let clientProc;
- try {
- if (!skipStart) {
- console.log('[shots] Starting local server and client');
- serverProc = startProcess('server', ['--filter', '@media-manager/server', 'dev']);
- clientProc = startProcess('client', ['--filter', '@media-manager/client', 'exec', 'vite', '--host', '127.0.0.1', '--port', '15173']);
- }
- await waitForUrl(`${apiBase}/api/health`, 90000);
- await waitForUrl(uiBase, 90000);
- await ensureRegressionUser();
- await capturePages(outputDir);
- console.log(`[shots] Done: ${outputDir}`);
- } finally {
- await stopProcess(clientProc);
- await stopProcess(serverProc);
- }
- }
- main().catch((error) => {
- console.error('[shots] Failed');
- console.error(error);
- process.exitCode = 1;
- });
|