Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
430
frontend/tests/e2e/apikey.spec.ts
Normal file
430
frontend/tests/e2e/apikey.spec.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* API Key Management Tests based on feature_list.json
|
||||
* Covers test cases: 18-29
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
await page.fill('#username', 'testuser');
|
||||
await page.fill('#password', 'Test123456');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(`${BASE_URL}/`, { timeout: 10000 });
|
||||
}
|
||||
|
||||
async function openCanvasSettings(page: Page) {
|
||||
await page.goto(`${BASE_URL}/canvas`);
|
||||
// Open settings panel - adjust selector based on actual UI
|
||||
await page.click('[data-testid="settings-button"], button:has-text("设置"), [aria-label*="设置"], [title*="设置"]');
|
||||
// Wait for settings panel to open
|
||||
await page.waitForSelector('[role="dialog"], .settings-panel, [data-testid="settings-panel"]', { timeout: 5000 });
|
||||
}
|
||||
|
||||
test.describe('API Key Page', () => {
|
||||
|
||||
// Test 18: API Key 页面正常显示 (在画布设置面板中)
|
||||
test('should display API Key management page correctly', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Verify Add Key button exists
|
||||
await expect(page.locator('button:has-text("添加 Key")').first()).toBeVisible();
|
||||
|
||||
// Verify refresh button exists (has RefreshCw icon)
|
||||
await expect(page.locator('button svg[class*="lucide-refresh"]').or(
|
||||
page.locator('button:has(.animate-spin)')
|
||||
).first()).toBeVisible();
|
||||
});
|
||||
|
||||
// Test 19: 添加 API Key 表单弹出
|
||||
test('should open add API Key form dialog', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Click Add Key button
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
|
||||
// Verify dialog opens (DialogContent with role="dialog")
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Verify dialog title
|
||||
await expect(page.locator('[role="dialog"] h2').or(
|
||||
page.locator('[role="dialog"] [class*="DialogTitle"]')
|
||||
).first()).toContainText(/添加|添加 API Key/i);
|
||||
|
||||
// Verify Provider dropdown exists (SelectTrigger)
|
||||
await expect(page.locator('[role="dialog"] button:has-text("选择")').or(
|
||||
page.locator('[role="dialog"] [class*="SelectTrigger"]')
|
||||
).first()).toBeVisible();
|
||||
|
||||
// Click cancel to close (AlertDialogCancel or close button)
|
||||
await page.click('button:has-text("取消")');
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
// Test 20: 添加单 Key 提供商 API Key
|
||||
test('should add single key provider API Key', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Click Add Key
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
|
||||
// Wait for dialog
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Open provider select dropdown
|
||||
await page.click('[role="dialog"] [class*="SelectTrigger"], [role="dialog"] button:has-text("选择")');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Select OpenAI provider
|
||||
await page.click('text=OpenAI');
|
||||
|
||||
// Fill in name
|
||||
await page.fill('[role="dialog"] input[type="text"]:not([name="apiKey"]):not([name="accessKey"]):not([name="secretKey"])', 'My OpenAI Key');
|
||||
|
||||
// Fill in API Key (primary field)
|
||||
await page.fill('[role="dialog"] input[name="apiKey"]', 'sk-test1234567890abcdef');
|
||||
|
||||
// Click add/submit button
|
||||
await page.click('[role="dialog"] button[type="submit"], [role="dialog"] button:has-text("添加")');
|
||||
|
||||
// Verify dialog closes
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// Test 21: 添加双 Key 提供商 API Key (Kling)
|
||||
test('should add dual key provider API Key (Kling)', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Click Add Key
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Open provider select
|
||||
await page.click('[role="dialog"] [class*="SelectTrigger"]');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Select Kling provider (可灵 AI)
|
||||
await page.click('text=/可灵|Kling/i');
|
||||
|
||||
// Wait for dual key fields to appear
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Verify two input fields for dual key provider
|
||||
const inputs = page.locator('[role="dialog"] input[type="text"], [role="dialog"] input[type="password"]');
|
||||
await expect(inputs).toHaveCount(3); // name + accessKey + secretKey (or similar)
|
||||
|
||||
// Fill in details
|
||||
await page.fill('[role="dialog"] input[type="text"]:not([name="apiKey"]):first-of-type', 'My Kling Key');
|
||||
|
||||
// Fill access and secret keys (order may vary)
|
||||
const textInputs = page.locator('[role="dialog"] input[type="text"]');
|
||||
const count = await textInputs.count();
|
||||
|
||||
for (let i = 0; i < count && i < 3; i++) {
|
||||
const input = textInputs.nth(i);
|
||||
const isVisible = await input.isVisible().catch(() => false);
|
||||
if (isVisible) {
|
||||
await input.fill(`test-key-${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit
|
||||
await page.click('[role="dialog"] button[type="submit"], [role="dialog"] button:has-text("添加")');
|
||||
|
||||
// Verify dialog closes
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// Test 22: API Key 脱敏显示
|
||||
test('should mask API Key display', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Add a key first
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page.click('[role="dialog"] [class*="SelectTrigger"]');
|
||||
await page.waitForTimeout(200);
|
||||
await page.click('text=OpenAI');
|
||||
|
||||
await page.fill('[role="dialog"] input[type="text"]:not([name="apiKey"]):first-of-type', 'Masked Key Test');
|
||||
await page.fill('[role="dialog"] input[name="apiKey"]', 'sk-test1234567890abcdef');
|
||||
await page.click('[role="dialog"] button[type="submit"]');
|
||||
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify masked key display (contains ***)
|
||||
const maskedKey = page.locator('code:has-text("***")').first();
|
||||
await expect(maskedKey).toBeVisible();
|
||||
|
||||
// Full key should not be visible in plain text
|
||||
await expect(page.locator('text=sk-test1234567890abcdef').first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
// Test 23: 编辑 API Key
|
||||
test('should edit API Key', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Wait for list to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find and click edit button (Edit2 icon)
|
||||
const editButton = page.locator('button svg[class*="lucide-edit"]').or(
|
||||
page.locator('button:has([class*="lucide-edit2"])')
|
||||
).first();
|
||||
|
||||
if (await editButton.isVisible().catch(() => false)) {
|
||||
await editButton.click();
|
||||
|
||||
// Verify form opens
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Modify name
|
||||
await page.fill('[role="dialog"] input[type="text"]:not([name="apiKey"]):not([name="accessKey"]):first-of-type', 'Updated Key Name');
|
||||
|
||||
// Save
|
||||
await page.click('[role="dialog"] button[type="submit"], [role="dialog"] button:has-text("保存")');
|
||||
|
||||
// Verify updated name appears
|
||||
await expect(page.locator('h3:has-text("Updated Key Name")').first()).toBeVisible({ timeout: 5000 });
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// Test 24: 删除 API Key
|
||||
test('should delete API Key', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Wait for list to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find first key card
|
||||
const firstCard = page.locator('.card, [class*="Card"]').filter({ has: page.locator('h3') }).first();
|
||||
|
||||
if (await firstCard.isVisible().catch(() => false)) {
|
||||
// Get key name for verification
|
||||
const keyName = await firstCard.locator('h3').textContent();
|
||||
|
||||
// Click delete button (Trash2 icon)
|
||||
const deleteButton = firstCard.locator('button svg[class*="lucide-trash"]').or(
|
||||
firstCard.locator('button:has([class*="lucide-trash2"])')
|
||||
).first();
|
||||
|
||||
if (await deleteButton.isVisible().catch(() => false)) {
|
||||
await deleteButton.click();
|
||||
|
||||
// Verify confirmation dialog appears
|
||||
await expect(page.locator('text=/确认删除|Confirm delete/i').first()).toBeVisible();
|
||||
|
||||
// Confirm delete
|
||||
await page.click('button:has-text("删除"), button:has-text("Confirm"), [class*="AlertDialogAction"]');
|
||||
|
||||
// Verify key is removed
|
||||
await expect(page.locator(`h3:has-text("${keyName}")`).first()).not.toBeVisible({ timeout: 5000 });
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// Test 25: 复制 API Key
|
||||
test('should copy API Key to clipboard', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Wait for list to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find copy button (Copy icon)
|
||||
const copyButton = page.locator('button svg[class*="lucide-copy"]').first();
|
||||
|
||||
if (await copyButton.isVisible().catch(() => false)) {
|
||||
await copyButton.click();
|
||||
|
||||
// Verify copy success - icon changes to Check
|
||||
await expect(page.locator('button svg[class*="lucide-check"]').or(
|
||||
page.locator('svg[class*="text-green"]').or(
|
||||
page.locator('svg.text-green-500')
|
||||
)
|
||||
).first()).toBeVisible({ timeout: 3000 });
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// Test 26: 刷新 API Key 列表
|
||||
test('should refresh API Key list', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Click refresh button (button with RefreshCw icon, not the "添加 Key" button)
|
||||
const refreshButton = page.locator('button').filter({ has: page.locator('svg[class*="lucide-refresh"]') }).first();
|
||||
|
||||
if (await refreshButton.isVisible().catch(() => false)) {
|
||||
await refreshButton.click();
|
||||
|
||||
// Verify loading state (spinning icon)
|
||||
await expect(page.locator('svg.animate-spin').first()).toBeVisible({ timeout: 2000 });
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// Test 27: API Key 表单验证
|
||||
test('should validate API Key form', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Click Add Key
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Try to submit without selecting provider
|
||||
await page.click('[role="dialog"] button[type="submit"], [role="dialog"] button:has-text("添加")');
|
||||
|
||||
// Verify validation error or dialog still open
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Close dialog
|
||||
await page.click('button:has-text("取消")');
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
// Test 28: 取消添加 API Key
|
||||
test('should cancel adding API Key', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Count initial cards
|
||||
const initialCount = await page.locator('.card, [class*="Card"]').filter({ has: page.locator('h3') }).count();
|
||||
|
||||
// Click Add Key
|
||||
await page.click('button:has-text("添加 Key")');
|
||||
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Select provider and fill some data
|
||||
await page.click('[role="dialog"] [class*="SelectTrigger"]');
|
||||
await page.waitForTimeout(200);
|
||||
await page.click('text=OpenAI');
|
||||
await page.fill('[role="dialog"] input[name="apiKey"]', 'sk-test-key');
|
||||
|
||||
// Click cancel
|
||||
await page.click('button:has-text("取消")');
|
||||
|
||||
// Verify dialog closes
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
// Test 29: 启用/禁用 API Key
|
||||
test('should toggle API Key enabled state', async ({ page }) => {
|
||||
await login(page);
|
||||
await openCanvasSettings(page);
|
||||
// Click on API Keys tab if exists
|
||||
const apiKeysTab = page.locator('button:has-text("API Key"), [role="tab"]:has-text("API Key")').first();
|
||||
if (await apiKeysTab.isVisible().catch(() => false)) {
|
||||
await apiKeysTab.click();
|
||||
}
|
||||
|
||||
// Wait for list to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find first card with checkbox
|
||||
const firstCard = page.locator('.card, [class*="Card"]').filter({ has: page.locator('h3') }).first();
|
||||
const checkbox = firstCard.locator('[role="checkbox"], input[type="checkbox"]').first();
|
||||
|
||||
if (await checkbox.isVisible().catch(() => false)) {
|
||||
// Get initial state
|
||||
const initialChecked = await checkbox.isChecked().catch(() => true);
|
||||
|
||||
// Click checkbox
|
||||
await checkbox.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify card opacity changes when disabled
|
||||
if (!initialChecked) {
|
||||
await expect(firstCard).toHaveClass(/opacity-60/);
|
||||
}
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
});
|
||||
227
frontend/tests/e2e/auth.spec.ts
Normal file
227
frontend/tests/e2e/auth.spec.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Authentication Tests based on feature_list.json
|
||||
* Covers test cases: 5, 7-8, 14-16, 31-34, 35-37
|
||||
*/
|
||||
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:3000';
|
||||
|
||||
// Helper to login
|
||||
async function login(page: Page) {
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
await page.fill('#username', 'testuser');
|
||||
await page.fill('#password', 'Test123456');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(`${BASE_URL}/`, { timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe('Authentication Flow', () => {
|
||||
|
||||
// Test 5: 登录成功
|
||||
test('should login successfully with valid credentials', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
|
||||
// Fill in credentials using id selectors
|
||||
await page.fill('#username', 'testuser');
|
||||
await page.fill('#password', 'Test123456');
|
||||
|
||||
// Click login and check loading state
|
||||
const loginButton = page.locator('button[type="submit"]');
|
||||
await loginButton.click();
|
||||
|
||||
// Verify loading state
|
||||
await expect(loginButton).toContainText('登录中');
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForURL(`${BASE_URL}/`, { timeout: 10000 });
|
||||
|
||||
// Verify redirected to home
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
|
||||
// Verify user avatar is visible in navbar
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await expect(avatar).toBeVisible();
|
||||
});
|
||||
|
||||
// Test 7: 已登录用户访问登录页自动重定向
|
||||
test('should redirect logged-in user from login page to home', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Try to access login page again
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
|
||||
// Should be redirected to home
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
});
|
||||
|
||||
// Test 8: 登录后重定向到原页面
|
||||
test('should redirect to original page after login', async ({ page }) => {
|
||||
// Access protected page while logged out
|
||||
await page.goto(`${BASE_URL}/canvas`);
|
||||
|
||||
// Should be redirected to login with redirect param
|
||||
await expect(page).toHaveURL(/.*login.*/);
|
||||
await expect(page).toHaveURL(/.*redirect=.*/);
|
||||
|
||||
// Login
|
||||
await page.fill('#username', 'testuser');
|
||||
await page.fill('#password', 'Test123456');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect back to original page
|
||||
await page.waitForURL(`${BASE_URL}/canvas`, { timeout: 10000 });
|
||||
await expect(page).toHaveURL(`${BASE_URL}/canvas`);
|
||||
});
|
||||
|
||||
// Test 14: 注册失败 - 用户名已存在
|
||||
test('should show error for existing username during registration', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/register`);
|
||||
|
||||
await page.fill('#username', 'testuser');
|
||||
await page.fill('#email', 'unique@example.com');
|
||||
await page.fill('#password', 'Test123456');
|
||||
await page.fill('#confirmPassword', 'Test123456');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show error about existing user
|
||||
await expect(page.locator('text=/already registered|已注册|用户名已存在|Username already exists/i').first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// Test 15: 注册成功
|
||||
test('should register successfully with new user', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/register`);
|
||||
|
||||
const timestamp = Date.now();
|
||||
await page.fill('#username', `newuser${timestamp}`);
|
||||
await page.fill('#email', `newuser${timestamp}@example.com`);
|
||||
await page.fill('#password', 'Test123456');
|
||||
await page.fill('#confirmPassword', 'Test123456');
|
||||
|
||||
const registerButton = page.locator('button[type="submit"]');
|
||||
await registerButton.click();
|
||||
|
||||
// Verify loading state
|
||||
await expect(registerButton).toContainText('注册中');
|
||||
|
||||
// Wait for navigation to home
|
||||
await page.waitForURL(`${BASE_URL}/`, { timeout: 10000 });
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
|
||||
// Verify avatar is visible
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await expect(avatar).toBeVisible();
|
||||
});
|
||||
|
||||
// Test 16: 已登录用户访问注册页自动重定向
|
||||
test('should redirect logged-in user from register page to home', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Try to access register page
|
||||
await page.goto(`${BASE_URL}/register`);
|
||||
|
||||
// Should be redirected to home
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('User Menu', () => {
|
||||
|
||||
// Test 31: 已登录状态显示用户头像
|
||||
test('should display user avatar when logged in', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Check avatar is visible
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await expect(avatar).toBeVisible();
|
||||
|
||||
// Check it shows first letter of username
|
||||
await expect(avatar).toContainText('T'); // 'testuser' starts with T
|
||||
});
|
||||
|
||||
// Test 32: 点击头像显示下拉菜单
|
||||
test('should show dropdown menu when clicking avatar', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Click avatar
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await avatar.click();
|
||||
|
||||
// Verify dropdown shows username and email
|
||||
await expect(page.locator('text=testuser').first()).toBeVisible();
|
||||
await expect(page.locator('text=/API Keys|API 密钥/i').first()).toBeVisible();
|
||||
await expect(page.locator('text=/Log out|退出|登出/i').first()).toBeVisible();
|
||||
});
|
||||
|
||||
// Test 33: 点击 API Keys 菜单项跳转
|
||||
test('should navigate to API Keys page from dropdown', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Click avatar
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await avatar.click();
|
||||
|
||||
// Click API Keys
|
||||
await page.click('text=/API Keys|API 密钥/i');
|
||||
|
||||
// Should navigate to api-keys page
|
||||
await expect(page).toHaveURL(`${BASE_URL}/canvas`);
|
||||
});
|
||||
|
||||
// Test 34: 点击 Log out 登出
|
||||
test('should logout when clicking logout', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Click avatar
|
||||
const avatar = page.locator('[data-testid="user-avatar"]').or(page.locator('.avatar')).first();
|
||||
await avatar.click();
|
||||
|
||||
// Click logout
|
||||
await page.click('text=/Log out|退出|登出/i');
|
||||
|
||||
// Should redirect to login or home
|
||||
await page.waitForLoadState('networkidle');
|
||||
const url = page.url();
|
||||
expect(url.includes('/login') || url === `${BASE_URL}/`).toBeTruthy();
|
||||
|
||||
// Verify login icon is shown (user is logged out)
|
||||
const loginIcon = page.locator('[data-testid="login-icon"]').or(page.locator('a[href="/login"]')).first();
|
||||
await expect(loginIcon).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Route Protection', () => {
|
||||
|
||||
// Test 35: 访问 /settings/* 需要登录
|
||||
test('should redirect to login when accessing settings without auth', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/canvas`);
|
||||
|
||||
// Should be redirected to login
|
||||
await expect(page).toHaveURL(/.*login.*/);
|
||||
});
|
||||
|
||||
// Test 36: 已登录用户访问登录/注册页重定向
|
||||
test('should redirect authenticated users from auth pages', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// Try accessing login
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
|
||||
// Try accessing register
|
||||
await page.goto(`${BASE_URL}/register`);
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
});
|
||||
|
||||
// Test 37: 访问公开页面无需登录
|
||||
test('should allow access to public pages without auth', async ({ page }) => {
|
||||
await page.goto(`${BASE_URL}/`);
|
||||
|
||||
// Should stay on home page
|
||||
await expect(page).toHaveURL(`${BASE_URL}/`);
|
||||
|
||||
// Should not redirect to login
|
||||
await expect(page).not.toHaveURL(/.*login.*/);
|
||||
});
|
||||
});
|
||||
151
frontend/tests/e2e/project-canvas.spec.ts
Normal file
151
frontend/tests/e2e/project-canvas.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Project Canvas Baseline Flow', () => {
|
||||
test('enter project, load canvas, edit prompt node, and trigger save', async ({ page }) => {
|
||||
const savePayloads: Array<Record<string, unknown>> = [];
|
||||
|
||||
await page.route('**/api/v1/**', async (route) => {
|
||||
const req = route.request();
|
||||
const url = new URL(req.url());
|
||||
const path = url.pathname;
|
||||
const method = req.method();
|
||||
|
||||
if (method === 'GET' && path === '/api/v1/config/models') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: { image: {}, video: {}, llm: {}, audio: {} },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'GET' && path === '/api/v1/projects/e2e-project') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: 'e2e-project',
|
||||
name: 'E2E Project',
|
||||
type: 'video',
|
||||
status: 'active',
|
||||
created_at: '2026-01-01T00:00:00.000Z',
|
||||
updated_at: '2026-01-01T00:00:00.000Z',
|
||||
episodes: [],
|
||||
assets: [],
|
||||
storyboards: [],
|
||||
generalCanvases: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'GET' && path === '/api/v1/projects/e2e-project/canvases') {
|
||||
const canvasType = url.searchParams.get('canvas_type');
|
||||
if (canvasType === 'general') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 0, message: 'ok', data: [] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'POST' && path === '/api/v1/projects/e2e-project/canvases') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: 'canvas-main-1',
|
||||
projectId: 'e2e-project',
|
||||
canvasType: 'general',
|
||||
relatedEntityType: null,
|
||||
relatedEntityId: null,
|
||||
name: '通用画布',
|
||||
description: '',
|
||||
orderIndex: 0,
|
||||
isPinned: false,
|
||||
tags: [],
|
||||
nodeCount: 0,
|
||||
accessCount: 0,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'GET' && path === '/api/v1/canvas') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: 'canvas-main-1',
|
||||
nodes: [],
|
||||
connections: [],
|
||||
history: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'POST' && path === '/api/v1/canvas') {
|
||||
const payload = req.postDataJSON() as Record<string, unknown>;
|
||||
savePayloads.push(payload);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 0, message: 'ok', data: { id: 'canvas-main-1' } }),
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 0, message: 'ok', data: {} }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/project/e2e-project');
|
||||
|
||||
await expect(
|
||||
page.getByPlaceholder('有什么可以帮您?(可输入 @ 选择素材)')
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId('canvas-add-node-toggle').click();
|
||||
await page.getByTestId('add-node-prompt_input').click();
|
||||
|
||||
const promptTextarea = page.getByTestId('prompt-input-textarea');
|
||||
await expect(promptTextarea).toBeVisible();
|
||||
await promptTextarea.fill('E2E: prompt update should be persisted');
|
||||
|
||||
await page.waitForTimeout(900);
|
||||
await page.keyboard.press('Control+S');
|
||||
|
||||
await expect.poll(() => savePayloads.length, { timeout: 10000 }).toBeGreaterThan(0);
|
||||
|
||||
await expect.poll(
|
||||
() =>
|
||||
savePayloads.some((payload) => {
|
||||
const nodes = (payload.nodes ?? []) as Array<{ data?: { type?: string; prompt?: string } }>;
|
||||
return nodes.some(
|
||||
(node) =>
|
||||
node.data?.type === 'PROMPT_INPUT'
|
||||
&& node.data?.prompt?.includes('E2E: prompt update should be persisted')
|
||||
);
|
||||
}),
|
||||
{ timeout: 10000 }
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user