import { test, expect, type Page } from '@playwright/test'; const PASSWORD = 'test123'; const UNIQUE = Date.now().toString(36); let pid = ''; let lastNewPwd = ''; async function login(page: Page, password = PASSWORD) { await page.goto('/login'); await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); await page.waitForURL('/'); } // ══════════════════════════════════════ // K7: 改密流程(httpOnly cookie + 密钥轮换 → 需重登) // ══════════════════════════════════════ test.describe('K7 改密流程', () => { test.afterAll(async ({ request }) => { // 崩溃安全:若改密测试中途失败,确保服务器密码还原,避免级联影响其他 spec const ok = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } }); if (ok.ok()) return; if (!lastNewPwd) return; const loginNew = await request.post('http://localhost:3002/api/auth/login', { data: { password: lastNewPwd } }); if (!loginNew.ok()) return; const { token } = await loginNew.json(); await request.post('http://localhost:3002/api/auth/password', { headers: { Authorization: `Bearer ${token}` }, data: { currentPassword: lastNewPwd, newPassword: PASSWORD }, }); }); test('改密:当前密码错误时显示错误且不跳转', async ({ page }) => { await login(page); await page.click('button[title="修改管理密码"]'); await page.fill('input[placeholder="当前密码"]', 'wrong-current'); await page.fill('input[placeholder="新密码(至少 6 位)"]', 'new-pass-123'); await page.click('.new-project-actions button:has-text("保存")'); await expect(page.locator('.new-project-form')).toContainText('当前密码错误'); await expect(page).toHaveURL('/'); }); test('改密成功后旧密码失效、新密码可登录,并还原原密码', async ({ page }) => { const NEW_PWD = `new-${UNIQUE}-x9`; lastNewPwd = NEW_PWD; await login(page, PASSWORD); await page.click('button[title="修改管理密码"]'); await page.fill('input[placeholder="当前密码"]', PASSWORD); await page.fill('input[placeholder="新密码(至少 6 位)"]', NEW_PWD); await page.click('.new-project-actions button:has-text("保存")'); await page.waitForURL('/login'); // 旧密码登录失败 await page.fill('input[type="password"]', PASSWORD); await page.click('button[type="submit"]'); await expect(page.locator('.error')).toContainText('密码错误'); // 新密码登录成功 await login(page, NEW_PWD); await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review'); // 还原原密码 await page.click('button[title="修改管理密码"]'); await page.fill('input[placeholder="当前密码"]', NEW_PWD); await page.fill('input[placeholder="新密码(至少 6 位)"]', PASSWORD); await page.click('.new-project-actions button:has-text("保存")'); await page.waitForURL('/login'); await login(page, PASSWORD); await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review'); }); }); // ══════════════════════════════════════ // D2: CSV 导入模板下载 // ══════════════════════════════════════ test.describe('D2 CSV 模板下载', () => { test.beforeAll(async ({ request }) => { const loginRes = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } }); const { token } = await loginRes.json(); const proj = await request.post('http://localhost:3002/api/projects', { headers: { Authorization: `Bearer ${token}` }, data: { name: `csv-${UNIQUE}`, track: '赛道一' }, }); pid = (await proj.json()).id; }); test('批量导入面板可下载 CSV 模板', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('button:has-text("批量导入")'); const downloadPromise = page.waitForEvent('download'); await page.click('button:has-text("下载模板")'); const download = await downloadPromise; expect(download.suggestedFilename()).toBe('entry-import-template.csv'); }); }); // ══════════════════════════════════════ // K4: 条目编辑保存补齐 sub_type/question_id // ══════════════════════════════════════ test.describe('K4 编辑保存字段', () => { let pid = ''; let eid = ''; test.beforeAll(async ({ request }) => { const loginRes = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } }); const { token } = await loginRes.json(); const proj = await request.post('http://localhost:3002/api/projects', { headers: { Authorization: `Bearer ${token}` }, data: { name: `k4-${UNIQUE}`, track: '赛道一' }, }); pid = (await proj.json()).id; const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, data: { title: 'k4-entry', repo_url: `file://C:\\k4-${UNIQUE}` }, }); eid = (await entry.json()).id; }); test('编辑保存 sub_type 持久化', async ({ page }) => { await login(page); await page.goto(`/project/${pid}`); await page.click('button:has-text("编辑")'); await page.selectOption('.detail-overlay select', { label: '新規开发' }); await page.click('.detail-overlay button:has-text("保存")'); const loginRes = await page.request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } }); const { token } = await loginRes.json(); const detailRes = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/${eid}`, { headers: { Authorization: `Bearer ${token}` }, }); const detail = await detailRes.json(); expect(detail.sub_type).toBe('新規'); }); });