import { chromium } from 'playwright'; import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs'; const ROOT = 'D:\\Projects\\ai-review'; const SERVER_DIR = path.join(ROOT, 'server'); const WEB_DIR = path.join(ROOT, 'web'); const SCREENSHOTS = 'C:\\Users\\NB-076\\AppData\\Local\\Temp\\opencode\\screenshots'; fs.mkdirSync(SCREENSHOTS, { recursive: true }); async function waitForPort(port, timeout = 15000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const res = await fetch(`http://localhost:${port}`); if (res.ok || res.status < 500) return true; } catch {} await new Promise(r => setTimeout(r, 500)); } throw new Error(`Port ${port} not ready in ${timeout}ms`); } async function main() { // Start backend const server = spawn('npx.cmd', ['tsx', 'src/index.ts'], { cwd: SERVER_DIR, stdio: 'pipe', shell: true, env: { ...process.env } }); server.stdout.on('data', d => process.stdout.write(`[server] ${d}`)); // Start frontend const frontend = spawn('npx.cmd', ['vite', '--port', '14001'], { cwd: WEB_DIR, stdio: 'pipe', shell: true, }); frontend.stdout.on('data', d => process.stdout.write(`[web] ${d}`)); console.log('Waiting for services...'); await waitForPort(3002); await waitForPort(14001); console.log('Both services ready'); // Get password from .env const envContent = fs.readFileSync(path.join(SERVER_DIR, '.env'), 'utf-8'); const pwdMatch = envContent.match(/AUTH_PASSWORD=(.+)/); const password = pwdMatch ? pwdMatch[1].trim() : '620f4c96'; console.log(`Using password: ${password}`); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { // Step 1: Login page console.log('\n=== Step 1: Login ==='); await page.goto('http://localhost:14001/login', { waitUntil: 'networkidle' }); await page.waitForTimeout(500); await page.screenshot({ path: path.join(SCREENSHOTS, '01-login-page.png'), fullPage: true }); // Login await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); await page.waitForURL('**/'); await page.waitForTimeout(1000); console.log('Logged in successfully'); // Step 2: Dashboard await page.screenshot({ path: path.join(SCREENSHOTS, '02-dashboard.png'), fullPage: true }); console.log('Dashboard screenshot taken'); // Step 3: Create project console.log('\n=== Step 2: Create Project ==='); await page.click('.btn-new-project'); await page.waitForTimeout(300); await page.fill('.new-project-form input', 'Demo Project'); await page.screenshot({ path: path.join(SCREENSHOTS, '03-create-project-form.png'), fullPage: true }); await page.click('.new-project-actions button:first-child'); await page.waitForURL(/\/project\//); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SCREENSHOTS, '04-project-created.png'), fullPage: true }); console.log('Project created'); // Get project ID from URL const url = page.url(); const projectId = url.split('/').pop(); console.log(`Project ID: ${projectId}`); // Step 4: Upload standard console.log('\n=== Step 3: Upload Standard ==='); await page.click('.tab:has-text("标准")'); await page.waitForTimeout(500); await page.click('.section-header button'); await page.waitForTimeout(300); const standardContent = `## 代码质量(40分) 规范、可读、健壮 ## 架构设计(30分) 模块化、可扩展 ## 功能完整性(30分) 需求覆盖、边界处理`; await page.fill('.standard-form input:first-child', '通用评审标准'); await page.fill('.standard-form textarea', standardContent); await page.screenshot({ path: path.join(SCREENSHOTS, '05-standard-form.png'), fullPage: true }); await page.click('.form-actions button:first-child'); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SCREENSHOTS, '06-standard-created.png'), fullPage: true }); console.log('Standard uploaded'); // Step 5: Create entries via API for speed console.log('\n=== Step 4: Create Entries ==='); const entries = [ { title: 'Todo App', repo_url: 'https://github.com/tastejs/todomvc.git', participant: '张三', difficulty: '中等' }, { title: 'Express Example', repo_url: 'https://github.com/expressjs/express.git', participant: '李四', difficulty: '困难' }, ]; const entryIds = []; for (const entry of entries) { const res = await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${await getToken(page)}` }, body: JSON.stringify(entry), }); const data = await res.json(); entryIds.push(data.id); console.log(`Created entry: ${entry.title} (${data.id})`); } // Navigate to entries tab await page.click('.tab:has-text("条目")'); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SCREENSHOTS, '07-entries-list.png'), fullPage: true }); console.log('Entries visible'); // Step 6: Start review console.log('\n=== Step 5: Start Review ==='); const startBtns = await page.locator('.btn-action:has-text("启动")'); const btnCount = await startBtns.count(); console.log(`Found ${btnCount} start buttons`); if (btnCount > 0) { await startBtns.first().click(); await page.waitForTimeout(1500); await page.screenshot({ path: path.join(SCREENSHOTS, '08-review-started.png'), fullPage: true }); console.log('Review started'); // Wait a bit for AI review to process await page.waitForTimeout(3000); await page.reload(); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SCREENSHOTS, '09-after-review.png'), fullPage: true }); } // Step 7: Force review for demo (set entries to reviewed state) console.log('\n=== Step 6: Force Complete for Demo ==='); for (const eid of entryIds) { await fetch(`http://localhost:3002/api/projects/${projectId}/entries/${eid}/force-review`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${await getToken(page)}` }, body: JSON.stringify({ dimensions: [ { name: '代码质量', score: 35 + Math.floor(Math.random() * 10), maxScore: 40 }, { name: '架构设计', score: 22 + Math.floor(Math.random() * 8), maxScore: 30 }, { name: '功能完整性', score: 25 + Math.floor(Math.random() * 5), maxScore: 30 }, ] }), }); } // Refresh and go to summary await page.reload(); await page.waitForTimeout(500); await page.click('.tab:has-text("汇总")'); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SCREENSHOTS, '10-summary-view.png'), fullPage: true }); console.log('Summary view'); // Step 8: Detail panel await page.click('.tab:has-text("条目")'); await page.waitForTimeout(500); await page.reload(); await page.waitForTimeout(1000); const titleCells = await page.locator('.title-cell'); if (await titleCells.count() > 0) { await titleCells.first().click(); await page.waitForTimeout(500); await page.screenshot({ path: path.join(SCREENSHOTS, '11-detail-panel.png'), fullPage: true }); // Close detail await page.click('.btn-close'); await page.waitForTimeout(300); } console.log('\n=== All screenshots captured! ==='); console.log(`Screenshots in: ${SCREENSHOTS}`); } catch (err) { console.error('Error:', err.message); await page.screenshot({ path: path.join(SCREENSHOTS, 'error.png'), fullPage: true }); } finally { await browser.close(); server.kill(); frontend.kill(); } } async function getToken(page) { return await page.evaluate(() => localStorage.getItem('token')); } main().catch(console.error);