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 { await page.goto(`${BASE_URL}/admin/content/new`); await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('input[type="text"]', { state: 'visible', timeout: 10000 }); const titleInput = page.locator('input[type="text"]').first(); await titleInput.fill(contentData.title); const slugInput = page.locator('input[placeholder="url-slug"]'); await slugInput.fill(contentData.slug); const excerptTextarea = page.locator('textarea').first(); await excerptTextarea.fill(contentData.excerpt); const typeSelect = page.locator('select').first(); await typeSelect.selectOption(contentData.type); const statusSelect = page.locator('select').nth(1); await statusSelect.selectOption(contentData.status); const categoryInput = page.locator('input[placeholder="分类名称"]'); await categoryInput.fill(contentData.category); const publishButton = page.locator('button:has-text("发布")'); await publishButton.click(); await page.waitForResponse(resp => resp.url().includes('/api/admin/content') && (resp.request().method() === 'POST' || resp.request().method() === 'PUT'), { timeout: 15000 } ); await page.waitForURL(/\/admin\/content\/[a-zA-Z0-9]+/, { timeout: 10000 }); const url = page.url(); const match = url.match(/\/admin\/content\/([a-zA-Z0-9]+)/); return match ? match[1] : null; } async function deleteContent(page: Page, contentId: string) { await page.goto(`${BASE_URL}/admin/content`); await page.waitForLoadState('domcontentloaded'); await page.waitForSelector('table tbody tr', { state: 'visible', timeout: 10000 }); const contentRow = page.locator(`tr:has-text("${contentId}")`); if (await contentRow.count() > 0) { const deleteButton = contentRow.locator('button:has-text("删除")'); await deleteButton.click(); const confirmButton = page.locator('button:has-text("确认"), button:has-text("确定")'); if (await confirmButton.count() > 0) { await confirmButton.click(); await page.waitForResponse(resp => resp.url().includes('/api/admin/content') && resp.request().method() === 'DELETE', { timeout: 10000 } ); } } } test.describe('后台管理发布功能测试', () => { test.beforeEach(async ({ page }) => { await loginAsAdmin(page); }); test('TC-001: 创建新闻内容并发布', async ({ page }) => { const contentData = testContents[0]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); 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).toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-002: 创建产品内容并发布', async ({ page }) => { const contentData = testContents[1]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/products`); await page.waitForLoadState('networkidle'); const productCard = page.locator(`text="${contentData.title}"`); await expect(productCard).toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-003: 创建服务内容并发布', async ({ page }) => { const contentData = testContents[2]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/services`); await page.waitForLoadState('networkidle'); const serviceCard = page.locator(`text="${contentData.title}"`); await expect(serviceCard).toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-004: 创建案例内容并发布', async ({ page }) => { const contentData = testContents[3]; const contentId = await createContent(page, contentData); expect(contentId).not.toBeNull(); await page.goto(`${BASE_URL}/cases`); await page.waitForLoadState('networkidle'); const caseCard = page.locator(`text="${contentData.title}"`); await expect(caseCard).toBeVisible(); if (contentId) { await deleteContent(page, contentId); } }); test('TC-005: 保存为草稿', async ({ page }) => { const draftContent: ContentData = { type: 'news', title: `草稿测试-${Date.now()}`, slug: `draft-test-${Date.now()}`, excerpt: '这是草稿测试内容', content: '

草稿内容

', 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(); }); });