import { test, expect, Page } from '@playwright/test'; const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@novalon.cn'; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123456'; interface ContentData { type: 'news' | 'product' | 'service' | 'case'; title: string; slug: string; excerpt: string; content: string; category: string; tags: string[]; status: 'draft' | 'published' | 'archived'; } const testContents: ContentData[] = [ { type: 'news', title: `测试新闻-${Date.now()}`, slug: `test-news-${Date.now()}`, excerpt: '这是一条测试新闻的摘要内容', content: '
这是测试新闻的正文内容
包含多个段落
', category: '公司新闻', tags: ['测试', '自动化'], status: 'published', }, { type: 'product', title: `测试产品-${Date.now()}`, slug: `test-product-${Date.now()}`, excerpt: '这是一个测试产品的描述', content: '测试产品的详细介绍
', category: '软件产品', tags: ['产品', '测试'], status: 'published', }, { type: 'service', title: `测试服务-${Date.now()}`, slug: `test-service-${Date.now()}`, excerpt: '这是一个测试服务的描述', content: '测试服务的详细介绍
', category: '软件开发', tags: ['服务', '测试'], status: 'published', }, { type: 'case', title: `测试案例-${Date.now()}`, slug: `test-case-${Date.now()}`, excerpt: '这是一个测试案例的描述', content: '测试案例的详细介绍
', category: '企业服务', tags: ['案例', '测试'], status: 'published', }, ]; async function loginAsAdmin(page: Page) { await page.goto(`${BASE_URL}/admin/login`); await page.waitForLoadState('networkidle'); const emailInput = page.locator('input[name="email"], input[type="email"]'); const passwordInput = page.locator('input[name="password"], input[type="password"]'); const submitButton = page.locator('button[type="submit"]'); await emailInput.fill(ADMIN_EMAIL); await passwordInput.fill(ADMIN_PASSWORD); await submitButton.click(); await page.waitForURL(/\/admin(?!\/login)/, { timeout: 10000 }); await page.waitForLoadState('networkidle'); } async function createContent(page: Page, contentData: ContentData): Promise草稿内容
', category: '公司新闻', tags: ['草稿'], status: 'draft', }; const contentId = await createContent(page, draftContent); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/admin/content`); await page.waitForLoadState('networkidle'); const contentRow = page.locator(`tr:has-text("${draftContent.title}")`); await expect(contentRow).toBeVisible(); const statusBadge = contentRow.locator('td:has-text("草稿")'); await expect(statusBadge).toBeVisible(); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const newsCard = page.locator(`text="${draftContent.title}"`); await expect(newsCard).not.toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-006: 编辑已发布的内容', async ({ page }) => { const contentData = testContents[0]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/admin/content/${contentId}`); await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('input[type="text"]', { state: 'visible', timeout: 10000 }); const updatedTitle = `${contentData.title}-已修改`; const titleInput = page.locator('input[type="text"]').first(); await titleInput.fill(updatedTitle); const saveButton = page.locator('button:has-text("保存草稿")'); await saveButton.click(); await page.waitForResponse(resp => resp.url().includes(`/api/admin/content/${contentId}`) && resp.request().method() === 'PUT', { timeout: 15000 } ); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const updatedCard = page.locator(`text="${updatedTitle}"`); await expect(updatedCard).toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-007: 删除内容', async ({ page }) => { const contentData = testContents[0]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await deleteContent(page, contentId!); await page.goto(`${BASE_URL}/admin/content`); await page.waitForLoadState('networkidle'); const contentRow = page.locator(`tr:has-text("${contentData.title}")`); await expect(contentRow).not.toBeVisible(); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const newsCard = page.locator(`text="${contentData.title}"`); await expect(newsCard).not.toBeVisible(); }); test('TC-008: 归档内容', async ({ page }) => { const contentData = testContents[0]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/admin/content/${contentId}`); await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('select', { state: 'visible', timeout: 10000 }); const statusSelect = page.locator('select').nth(1); await statusSelect.selectOption('archived'); const saveButton = page.locator('button:has-text("保存草稿")'); await saveButton.click(); await page.waitForResponse(resp => resp.url().includes(`/api/admin/content/${contentId}`) && resp.request().method() === 'PUT', { timeout: 15000 } ); await page.goto(`${BASE_URL}/admin/content`); await page.waitForLoadState('networkidle'); const contentRow = page.locator(`tr:has-text("${contentData.title}")`); await expect(contentRow).toBeVisible(); const statusBadge = contentRow.locator('td:has-text("已归档")'); await expect(statusBadge).toBeVisible(); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const newsCard = page.locator(`text="${contentData.title}"`); await expect(newsCard).not.toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-015: 空内容提交验证', async ({ page }) => { await page.goto(`${BASE_URL}/admin/content/new`); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); const publishButton = page.locator('button:has-text("发布")'); await publishButton.click(); await page.waitForTimeout(1000); const errorMessage = page.locator('text=/请输入标题|标题不能为空|请输入|必填/'); await expect(errorMessage.first()).toBeVisible(); }); test('TC-018: 未登录用户访问后台', async ({ context }) => { const newPage = await context.newPage(); await newPage.goto(`${BASE_URL}/admin/content`); await newPage.waitForLoadState('networkidle'); expect(newPage.url()).toContain('/admin/login'); await newPage.close(); }); }); test.describe('前端内容展示验证', () => { test('新闻页面加载正常', async ({ page }) => { await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); await expect(page.locator('h1, .page-header')).toContainText('新闻'); const newsCards = page.locator('article, .card, [class*="news-item"]'); const count = await newsCards.count(); expect(count).toBeGreaterThan(0); }); test('产品页面加载正常', async ({ page }) => { await page.goto(`${BASE_URL}/products`); await page.waitForLoadState('networkidle'); await expect(page.locator('h1, .page-header')).toContainText('产品'); const productCards = page.locator('article, .card, [class*="product"]'); const count = await productCards.count(); expect(count).toBeGreaterThan(0); }); test('服务页面加载正常', async ({ page }) => { await page.goto(`${BASE_URL}/services`); await page.waitForLoadState('networkidle'); await expect(page.locator('h1, .page-header')).toContainText('服务'); }); test('案例页面加载正常', async ({ page }) => { await page.goto(`${BASE_URL}/cases`); await page.waitForLoadState('networkidle'); await expect(page.locator('h1, .page-header')).toContainText('案例'); }); }); test.describe('性能测试', () => { test('TC-025: 后台列表加载性能', async ({ page }) => { await loginAsAdmin(page); const startTime = Date.now(); await page.goto(`${BASE_URL}/admin/content`); await page.waitForLoadState('networkidle'); const loadTime = Date.now() - startTime; console.log(`后台列表加载时间: ${loadTime}ms`); expect(loadTime).toBeLessThan(3000); }); test('前端新闻页面加载性能', async ({ page }) => { const startTime = Date.now(); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const loadTime = Date.now() - startTime; console.log(`前端新闻页面加载时间: ${loadTime}ms`); expect(loadTime).toBeLessThan(3000); }); }); test.describe('安全测试', () => { test('TC-031: XSS攻击防护', async ({ page }) => { await loginAsAdmin(page); const xssContent: ContentData = { type: 'news', title: `XSS测试-${Date.now()}`, slug: `xss-test-${Date.now()}`, excerpt: '测试摘要', content: '测试内容
', category: '公司新闻', tags: ['安全测试'], status: 'published', }; const contentId = await createContent(page, xssContent); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); const xssTriggered = await page.evaluate(() => { return (window as any).xssTriggered === true; }); expect(xssTriggered).toBe(false); if (contentId) { await deleteContent(page, contentId); } }); test('TC-033: API权限验证', async ({ request }) => { const response = await request.post(`${BASE_URL}/api/admin/content`, { data: { type: 'news', title: '未授权测试', slug: 'unauthorized-test', content: '测试内容', }, }); expect(response.status()).toBe(403); }); }); test.describe('跨浏览器兼容性测试', () => { test('响应式设计 - 移动端', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); await expect(page.locator('header')).toBeVisible(); await expect(page.locator('footer')).toBeVisible(); }); test('响应式设计 - 平板端', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await page.goto(`${BASE_URL}/news`); await page.waitForLoadState('networkidle'); await expect(page.locator('header')).toBeVisible(); await expect(page.locator('footer')).toBeVisible(); }); });