Files
L2keka/web/demo-user-story.mjs
T

194 lines
8.5 KiB
JavaScript

import { chromium } from 'playwright';
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
const ROOT = 'D:\\Projects\\ai-review';
const SCREENSHOTS = 'C:\\Users\\NB-076\\AppData\\Local\\Temp\\opencode\\story';
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`);
}
async function ss(page, name) {
await page.screenshot({ path: path.join(SCREENSHOTS, name), fullPage: true });
console.log(` Screenshot: ${name}`);
}
async function main() {
// Start services
const server = spawn('npx.cmd', ['tsx', 'src/index.ts'], { cwd: path.join(ROOT, 'server'), stdio: 'pipe', shell: true });
const frontend = spawn('npx.cmd', ['vite', '--port', '14001'], { cwd: path.join(ROOT, 'web'), stdio: 'pipe', shell: true });
server.stdout.on('data', d => process.stdout.write(`[s] ${d}`));
frontend.stdout.on('data', d => process.stdout.write(`[f] ${d}`));
console.log('Waiting for services...');
await waitForPort(3002); await waitForPort(14001);
console.log('Ready!');
const envContent = fs.readFileSync(path.join(ROOT, 'server', '.env'), 'utf-8');
const password = (envContent.match(/AUTH_PASSWORD=(.+)/) || [,'620f4c96'])[1].trim();
const browser = await chromium.launch({ headless: false }); // visible so you can watch
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
// ═══════════════════════════════════════════════
// STORY: 技术大赛双赛道评审
// ═══════════════════════════════════════════════
// STEP 1: Login → Dashboard
console.log('\n=== 1. Login ===');
await page.goto('http://localhost:14001/login', { waitUntil: 'networkidle' });
await ss(page, '01-login.png');
await page.fill('input[type="password"]', password);
await page.click('button[type="submit"]');
await page.waitForURL('**/');
await page.waitForTimeout(1000);
await ss(page, '02-dashboard.png');
// STEP 2: Create Project "2026技术大赛"
console.log('\n=== 2. Create Project ===');
await page.click('.btn-new-project');
await page.waitForTimeout(300);
await page.fill('.new-project-form input', '2026技术大赛');
await ss(page, '03-create-project.png');
await page.click('.new-project-actions button:first-child');
await page.waitForURL(/\/project\//);
await page.waitForTimeout(500);
const projectUrl = page.url();
const projectId = projectUrl.split('/').pop();
console.log(`Project: ${projectId}`);
// STEP 3: Upload standards for both tracks
console.log('\n=== 3. Upload Standards ===');
await page.click('.tab:has-text("标准")');
await page.waitForTimeout(500);
const standard1 = fs.readFileSync(path.join(ROOT, 'docs', 'standards', '技术大赛-赛道一评审标准.md'), 'utf-8');
const standard2 = fs.readFileSync(path.join(ROOT, 'docs', 'standards', '技术大赛-赛道二评审标准.md'), 'utf-8');
// Upload 赛道一 standard
await page.click('.section-header button');
await page.waitForTimeout(300);
await page.fill('.standard-form input:first-child', '技术大赛-赛道一评审标准');
await page.fill('.standard-form input[placeholder*="标签"]', '赛道一');
await page.fill('.standard-form textarea', standard1);
await ss(page, '04-standard-track1-form.png');
await page.click('.form-actions button:first-child');
await page.waitForTimeout(1000);
// Upload 赛道二 standard
await page.click('.section-header button');
await page.waitForTimeout(300);
await page.fill('.standard-form input:first-child', '技术大赛-赛道二评审标准');
await page.fill('.standard-form input[placeholder*="标签"]', '赛道二');
await page.fill('.standard-form textarea', standard2);
await ss(page, '05-standard-track2-form.png');
await page.click('.form-actions button:first-child');
await page.waitForTimeout(1000);
await ss(page, '06-standards-list.png');
// STEP 4: Create entries via API (faster)
console.log('\n=== 4. Create Entries ===');
const token = await page.evaluate(() => localStorage.getItem('token'));
const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
// Real repos that actually exist
const entries = [
{ title: '张三 - 代码助手', repo_url: 'https://github.com/features/copilot', category_tag: '赛道一', participant: '张三', difficulty: '困难' },
{ title: '李四 - TDD工具', repo_url: 'https://github.com/testdouble/tdd-toolkit', category_tag: '赛道一', participant: '李四', difficulty: '中等' },
{ title: '王五 - IDE插件', repo_url: 'https://github.com/microsoft/vscode-extension-samples', category_tag: '赛道二', participant: '王五', difficulty: '困难' },
{ title: '赵六 - 范式创新', repo_url: 'https://github.com/features/copilot', category_tag: '赛道二', participant: '赵六', difficulty: '中等' },
];
for (const entry of entries) {
await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, {
method: 'POST', headers: auth,
body: JSON.stringify(entry),
});
}
await page.click('.tab:has-text("条目")');
await page.waitForTimeout(1000);
await ss(page, '07-entries-list.png');
console.log('4 entries created');
// STEP 5: Start all reviews via API (faster)
console.log('\n=== 5. Start Reviews via API ===');
for (const entry of entries) {
const listRes = await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, { headers: auth });
const { items } = await listRes.json();
const match = items.find(e => e.title === entry.title);
if (match && match.status === 'pending') {
await fetch(`http://localhost:3002/api/projects/${projectId}/entries/${match.id}/start`, {
method: 'POST', headers: auth,
});
console.log(` Started: ${entry.title}`);
}
}
await page.reload();
await page.waitForTimeout(3000);
await ss(page, '08-after-start.png');
// STEP 6: Force-complete for demo (since AI review needs real repos)
console.log('\n=== 6. Force Complete for Demo ===');
const res = await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, {
headers: auth,
});
const { items } = await res.json();
for (const entry of items) {
await fetch(`http://localhost:3002/api/projects/${projectId}/entries/${entry.id}/force-review`, {
method: 'PUT', headers: auth,
body: JSON.stringify({
dimensions: [
{ name: '开发范式设计清晰度', score: 3 + Math.floor(Math.random() * 3), maxScore: 5 },
{ name: 'IDE集成深度', score: 3 + Math.floor(Math.random() * 3), maxScore: 5 },
{ name: '提效幅度', score: 10 + Math.floor(Math.random() * 6), maxScore: 15 },
{ name: '稳定性与易用性', score: 8 + Math.floor(Math.random() * 7), maxScore: 15 },
{ name: 'AI使用日志', score: 3 + Math.floor(Math.random() * 2), maxScore: 10 },
{ name: '演示与文档', score: 5 + Math.floor(Math.random() * 5), maxScore: 10 },
]
}),
});
}
// STEP 7: Show Summary with track-based rankings
await page.reload();
await page.waitForTimeout(500);
await page.click('.tab:has-text("汇总")');
await page.waitForTimeout(1000);
await ss(page, '09-summary-tracks.png');
// STEP 8: Show detail panel
await page.click('.tab:has-text("条目")');
await page.waitForTimeout(500);
await page.reload();
await page.waitForTimeout(1000);
const titles = await page.locator('.title-cell');
if (await titles.count() > 0) {
await titles.first().click();
await page.waitForTimeout(800);
await ss(page, '10-detail-panel.png');
}
console.log('\n=== DONE ===');
console.log(`All screenshots: ${SCREENSHOTS}`);
console.log(`App running at: http://localhost:14001 (password: ${password})`);
} catch (err) {
console.error('Error:', err.message);
await page.screenshot({ path: path.join(SCREENSHOTS, 'error.png'), fullPage: true });
}
}
main().catch(console.error);