capture-ui-screenshots.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { spawn } from 'node:child_process';
  2. import { mkdir, writeFile } from 'node:fs/promises';
  3. import { existsSync, readFileSync } from 'node:fs';
  4. import { dirname, join, resolve } from 'node:path';
  5. import { fileURLToPath } from 'node:url';
  6. import { chromium } from 'playwright';
  7. const __dirname = dirname(fileURLToPath(import.meta.url));
  8. const repoRoot = resolve(__dirname, '..');
  9. const apiBase = process.env.MM_API_BASE || resolveApiBase();
  10. const uiBase = process.env.MM_UI_BASE || 'http://127.0.0.1:15173';
  11. const username = process.env.MM_USERNAME || `codex_shots_${Date.now().toString(36)}`;
  12. const password = process.env.MM_PASSWORD || 'Codex123456';
  13. const skipStart = process.env.MM_SKIP_START === '1';
  14. const pages = [
  15. { name: 'dashboard', path: '/#/' },
  16. { name: 'accounts', path: '/#/accounts' },
  17. { name: 'works', path: '/#/works' },
  18. { name: 'publish', path: '/#/publish' },
  19. { name: 'analytics-overview', path: '/#/analytics/overview' },
  20. { name: 'analytics-platform', path: '/#/analytics/platform' },
  21. { name: 'analytics-account', path: '/#/analytics/account' },
  22. { name: 'analytics-work', path: '/#/analytics/work' },
  23. ];
  24. function resolveApiBase() {
  25. try {
  26. const envText = readFileSync(resolve(repoRoot, 'server', '.env'), 'utf8');
  27. const portMatch = envText.match(/^PORT=(.+)$/m);
  28. const hostMatch = envText.match(/^HOST=(.+)$/m);
  29. const port = portMatch?.[1]?.trim() || '3000';
  30. const rawHost = hostMatch?.[1]?.trim() || '127.0.0.1';
  31. const host = rawHost === '0.0.0.0' ? '127.0.0.1' : rawHost;
  32. return `http://${host}:${port}`;
  33. } catch {
  34. return 'http://127.0.0.1:3000';
  35. }
  36. }
  37. function getTimestamp() {
  38. return new Date().toISOString().replace(/[:.]/g, '-');
  39. }
  40. function createOutputDir() {
  41. const explicitDir = process.argv[2];
  42. return explicitDir
  43. ? resolve(repoRoot, explicitDir)
  44. : resolve(repoRoot, 'minimax-output', 'regression', getTimestamp());
  45. }
  46. function startProcess(label, args) {
  47. const child = spawnPnpm(args, {
  48. cwd: repoRoot,
  49. stdio: ['ignore', 'pipe', 'pipe'],
  50. windowsHide: true,
  51. });
  52. let logs = '';
  53. const appendLog = (chunk) => {
  54. logs += chunk.toString();
  55. if (logs.length > 6000) {
  56. logs = logs.slice(-6000);
  57. }
  58. };
  59. child.stdout.on('data', appendLog);
  60. child.stderr.on('data', appendLog);
  61. child.on('exit', (code) => {
  62. if (code && code !== 0) {
  63. console.error(`[${label}] exited with code ${code}`);
  64. console.error(logs);
  65. }
  66. });
  67. return { label, child, getLogs: () => logs };
  68. }
  69. async function stopProcess(proc) {
  70. if (!proc || proc.child.killed) return;
  71. proc.child.kill('SIGTERM');
  72. await new Promise((resolveStop) => setTimeout(resolveStop, 1500));
  73. if (!proc.child.killed && process.platform === 'win32') {
  74. spawn('taskkill', ['/PID', String(proc.child.pid), '/T', '/F'], {
  75. stdio: 'ignore',
  76. windowsHide: true,
  77. });
  78. }
  79. }
  80. async function waitForUrl(url, timeoutMs = 60000) {
  81. const start = Date.now();
  82. while (Date.now() - start < timeoutMs) {
  83. try {
  84. const response = await fetch(url);
  85. if (response.ok) return;
  86. } catch {
  87. // retry
  88. }
  89. await new Promise((resolveWait) => setTimeout(resolveWait, 1000));
  90. }
  91. throw new Error(`Timed out waiting for ${url}`);
  92. }
  93. async function ensurePlaywrightBrowser() {
  94. if (existsSync(chromium.executablePath())) return;
  95. console.log('[shots] Installing Playwright Chromium');
  96. await new Promise((resolveInstall, rejectInstall) => {
  97. const installer = spawnPnpm(['exec', 'playwright', 'install', 'chromium'], {
  98. cwd: repoRoot,
  99. stdio: 'inherit',
  100. windowsHide: true,
  101. });
  102. installer.on('exit', (code) => {
  103. if (code === 0) resolveInstall();
  104. else rejectInstall(new Error(`playwright install exited with code ${code}`));
  105. });
  106. });
  107. }
  108. function spawnPnpm(args, options) {
  109. if (process.platform === 'win32') {
  110. return spawn('cmd.exe', ['/d', '/s', '/c', 'pnpm', ...args], options);
  111. }
  112. return spawn('pnpm', args, options);
  113. }
  114. async function login(page) {
  115. await page.addInitScript((serverUrl) => {
  116. localStorage.setItem(
  117. 'server_configs',
  118. JSON.stringify([{ id: 'server_default', name: '本地 Node 服务', url: serverUrl, isDefault: true }]),
  119. );
  120. localStorage.setItem('current_server', 'server_default');
  121. }, apiBase);
  122. await page.goto(`${uiBase}/#/login`, { waitUntil: 'domcontentloaded' });
  123. await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
  124. await page.locator('.login-form input').nth(0).fill(username);
  125. await page.locator('.login-form input').nth(1).fill(password);
  126. await Promise.all([
  127. page.waitForURL(/#\/$/, { timeout: 30000 }),
  128. page.locator('.login-btn').click(),
  129. ]);
  130. await page.locator('.main-layout, .dashboard-page').first().waitFor({ timeout: 30000 });
  131. }
  132. async function capturePages(outputDir) {
  133. const browser = await chromium.launch({ headless: true });
  134. const context = await browser.newContext({
  135. viewport: { width: 1600, height: 1200 },
  136. deviceScaleFactor: 1,
  137. });
  138. const page = await context.newPage();
  139. await login(page);
  140. const manifest = [];
  141. for (const item of pages) {
  142. const targetPath = join(outputDir, `${item.name}.png`);
  143. console.log(`[shots] Capturing ${item.name}`);
  144. await page.goto(`${uiBase}${item.path}`, { waitUntil: 'domcontentloaded' });
  145. await page.locator('.splash-screen').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
  146. await page.waitForTimeout(1800);
  147. await page.screenshot({ path: targetPath, fullPage: true });
  148. manifest.push({ page: item.name, path: targetPath });
  149. }
  150. await writeFile(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
  151. await browser.close();
  152. }
  153. async function ensureRegressionUser() {
  154. if (process.env.MM_USERNAME && process.env.MM_PASSWORD) return;
  155. const payload = {
  156. username,
  157. password,
  158. email: `${username}@example.com`,
  159. };
  160. const response = await fetch(`${apiBase}/api/auth/register`, {
  161. method: 'POST',
  162. headers: { 'Content-Type': 'application/json' },
  163. body: JSON.stringify(payload),
  164. });
  165. if (response.ok) return;
  166. const text = await response.text();
  167. if (!text.includes('已存在') && !text.includes('exists')) {
  168. throw new Error(`Failed to create regression user: ${text}`);
  169. }
  170. }
  171. async function main() {
  172. const outputDir = createOutputDir();
  173. await mkdir(outputDir, { recursive: true });
  174. await ensurePlaywrightBrowser();
  175. let serverProc;
  176. let clientProc;
  177. try {
  178. if (!skipStart) {
  179. console.log('[shots] Starting local server and client');
  180. serverProc = startProcess('server', ['--filter', '@media-manager/server', 'dev']);
  181. clientProc = startProcess('client', ['--filter', '@media-manager/client', 'exec', 'vite', '--host', '127.0.0.1', '--port', '15173']);
  182. }
  183. await waitForUrl(`${apiBase}/api/health`, 90000);
  184. await waitForUrl(uiBase, 90000);
  185. await ensureRegressionUser();
  186. await capturePages(outputDir);
  187. console.log(`[shots] Done: ${outputDir}`);
  188. } finally {
  189. await stopProcess(clientProc);
  190. await stopProcess(serverProc);
  191. }
  192. }
  193. main().catch((error) => {
  194. console.error('[shots] Failed');
  195. console.error(error);
  196. process.exitCode = 1;
  197. });