diff --git a/e2e/admin-frontend-interaction.spec.ts b/e2e/admin-frontend-interaction.spec.ts deleted file mode 100644 index 969a09f..0000000 --- a/e2e/admin-frontend-interaction.spec.ts +++ /dev/null @@ -1,332 +0,0 @@ -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'; - -test.describe('后台与前台页面交互测试', () => { - test('首页展示所有内容类型入口', async ({ page }) => { - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - const navLinks = page.locator('nav a, header a[href]'); - const count = await navLinks.count(); - - console.log(`首页导航链接数量: ${count}`); - - expect(count).toBeGreaterThan(0); - - const linkTexts = await navLinks.allTextContents(); - console.log('导航链接:', linkTexts); - }); - - test('新闻页面内容展示', async ({ page }) => { - await page.goto(`${BASE_URL}/news`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/news/); - - const mainContent = page.locator('main, [role="main"]'); - await expect(mainContent).toBeVisible(); - - const heading = page.locator('h1, h2').first(); - const hasHeading = await heading.isVisible().catch(() => false); - console.log(`新闻页面标题${hasHeading ? '存在' : '不存在'}`); - }); - - test('产品页面内容展示', async ({ page }) => { - await page.goto(`${BASE_URL}/products`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/products/); - - const mainContent = page.locator('main, [role="main"]'); - await expect(mainContent).toBeVisible(); - }); - - test('服务页面内容展示', async ({ page }) => { - await page.goto(`${BASE_URL}/services`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/services/); - - const mainContent = page.locator('main, [role="main"]'); - await expect(mainContent).toBeVisible(); - }); - - test('案例页面内容展示', async ({ page }) => { - await page.goto(`${BASE_URL}/cases`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/cases/); - - const mainContent = page.locator('main, [role="main"]'); - await expect(mainContent).toBeVisible(); - }); -}); - -test.describe('后台内容管理功能测试', () => { - test.beforeEach(async ({ page }) => { - await page.goto(`${BASE_URL}/admin/login`); - await page.waitForLoadState('networkidle'); - - const emailInput = page.locator('#email'); - const passwordInput = page.locator('#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: 15000 }); - }); - - test('后台仪表盘加载', async ({ page }) => { - await page.goto(`${BASE_URL}/admin`); - await page.waitForLoadState('networkidle'); - - const heading = page.locator('h1, .text-2xl').first(); - await expect(heading).toBeVisible(); - - console.log('后台仪表盘加载成功'); - }); - - test('后台内容列表页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content`); - await page.waitForLoadState('networkidle'); - - const table = page.locator('table'); - await expect(table).toBeVisible(); - - const rows = page.locator('tbody tr'); - const count = await rows.count(); - console.log(`后台内容列表数量: ${count}`); - }); - - test('后台新建内容页面表单完整性', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content/new`); - await page.waitForLoadState('domcontentloaded'); - await page.waitForSelector('input[placeholder="请输入标题"]', { timeout: 60000 }); - - const titleInput = page.locator('input[placeholder="请输入标题"]'); - await expect(titleInput).toBeVisible(); - - const slugInput = page.locator('input[placeholder="url-slug"]'); - await expect(slugInput).toBeVisible(); - - const typeSelect = page.locator('select').first(); - await expect(typeSelect).toBeVisible(); - - const categoryInput = page.locator('input[placeholder="分类名称"]'); - const hasCategory = await categoryInput.isVisible().catch(() => false); - console.log(`分类输入框${hasCategory ? '存在' : '不存在'}`); - - const publishButton = page.locator('button:has-text("发布")'); - await expect(publishButton).toBeVisible(); - - const saveDraftButton = page.locator('button:has-text("保存草稿"), button:has-text("保存")'); - await expect(saveDraftButton).toBeVisible(); - }); - - test('后台内容编辑页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content`); - await page.waitForLoadState('networkidle'); - - const rows = page.locator('tbody tr'); - const count = await rows.count(); - - if (count > 0) { - const firstEditLink = page.locator('tbody tr:first-child a[href*="/admin/content/"]').first(); - const hasEditLink = await firstEditLink.isVisible().catch(() => false); - - if (hasEditLink) { - await firstEditLink.click(); - await page.waitForLoadState('domcontentloaded'); - - const titleInput = page.locator('input[placeholder="请输入标题"]'); - await expect(titleInput).toBeVisible({ timeout: 30000 }); - - console.log('编辑页面加载成功'); - } else { - console.log('没有可编辑的内容'); - } - } else { - console.log('内容列表为空'); - } - }); - - test('后台内容分类管理', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/categories`); - await page.waitForLoadState('networkidle'); - - const heading = page.locator('h1, .text-2xl').first(); - const hasHeading = await heading.isVisible().catch(() => false); - - console.log(`分类管理页面${hasHeading ? '可访问' : '不存在或无权限'}`); - }); -}); - -test.describe('内容导航和链接测试', () => { - test('导航到不同内容类型页面', async ({ page }) => { - const pages = [ - { url: '/news', name: '新闻' }, - { url: '/products', name: '产品' }, - { url: '/services', name: '服务' }, - { url: '/cases', name: '案例' }, - ]; - - for (const p of pages) { - await page.goto(`${BASE_URL}${p.url}`); - await page.waitForLoadState('networkidle'); - - const url = page.url(); - console.log(`${p.name}页面: ${url.includes(p.url) ? '可访问' : '不可访问'}`); - } - }); - - test('内容详情页访问', async ({ page }) => { - await page.goto(`${BASE_URL}/news`); - await page.waitForLoadState('networkidle'); - - const links = page.locator('a[href*="/news/"]'); - const count = await links.count(); - - if (count > 0) { - const firstLink = links.first(); - const href = await firstLink.getAttribute('href'); - - if (href && !href.startsWith('http')) { - await page.goto(`${BASE_URL}${href}`); - await page.waitForLoadState('networkidle'); - - const mainContent = page.locator('main, article'); - const isVisible = await mainContent.isVisible().catch(() => false); - console.log(`详情页加载${isVisible ? '成功' : '失败'}`); - } - } else { - console.log('没有可访问的新闻详情链接'); - } - }); -}); - -test.describe('SEO和元数据测试', () => { - test('页面标题验证', async ({ page }) => { - const pages = [ - { url: '/', name: '首页' }, - { url: '/news', name: '新闻' }, - { url: '/products', name: '产品' }, - ]; - - for (const p of pages) { - await page.goto(`${BASE_URL}${p.url}`); - await page.waitForLoadState('networkidle'); - - const title = await page.title(); - console.log(`${p.name}标题: ${title}`); - - expect(title.length).toBeGreaterThan(0); - } - }); - - test('Meta描述标签验证', async ({ page }) => { - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - const metaDesc = page.locator('meta[name="description"]'); - const hasMetaDesc = await metaDesc.count(); - - console.log(`Meta描述标签${hasMetaDesc > 0 ? '存在' : '不存在'}`); - }); -}); - -test.describe('响应式导航测试', () => { - test('移动端导航菜单', async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - const menuButton = page.locator('button[aria-label*="菜单"], button[class*="menu"], button[class*="Menu"]'); - const hasMenuButton = await menuButton.isVisible().catch(() => false); - - console.log(`移动端菜单按钮${hasMenuButton ? '存在' : '不存在'}`); - - if (hasMenuButton) { - await menuButton.click(); - await page.waitForSelector('nav, [class*="menu"], [class*="Menu"]', { state: 'visible', timeout: 5000 }); - - const navMenu = page.locator('nav, [class*="menu"], [class*="Menu"]'); - const isVisible = await navMenu.isVisible().catch(() => false); - console.log(`导航菜单${isVisible ? '展开' : '未展开'}`); - } - }); - - test('桌面端导航显示', async ({ page }) => { - await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - const navLinks = page.locator('nav a'); - const count = await navLinks.count(); - - console.log(`桌面端导航链接数量: ${count}`); - expect(count).toBeGreaterThan(0); - }); -}); - -test.describe('页面加载性能测试', () => { - test('各页面加载时间', async ({ page }) => { - const pages = [ - { url: '/', name: '首页' }, - { url: '/news', name: '新闻' }, - { url: '/products', name: '产品' }, - { url: '/services', name: '服务' }, - { url: '/cases', name: '案例' }, - ]; - - for (const p of pages) { - const startTime = Date.now(); - await page.goto(`${BASE_URL}${p.url}`); - await page.waitForLoadState('networkidle'); - const loadTime = Date.now() - startTime; - - console.log(`${p.name}页面加载时间: ${loadTime}ms`); - expect(loadTime).toBeLessThan(5000); - } - }); -}); - -test.describe('错误处理测试', () => { - test('访问不存在的页面', async ({ page }) => { - await page.goto(`${BASE_URL}/nonexistent-page-12345`); - await page.waitForLoadState('networkidle'); - - const errorElement = page.locator('[class*="error"], h1:has-text("404"), text=页面不存在'); - const hasError = await errorElement.isVisible().catch(() => false); - - console.log(`404页面${hasError ? '正确显示' : '未显示'}`); - }); - - test('后台访问无权限内容', async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - - await page.goto(`${BASE_URL}/admin/content/99999`); - await page.waitForLoadState('networkidle'); - await page.waitForURL(/\/admin/, { timeout: 5000 }); - - const url = page.url(); - console.log(`访问不存在内容后URL: ${url}`); - - await context.close(); - }); -}); - -test.describe('国际化支持测试', () => { - test('页面语言属性', async ({ page }) => { - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - const htmlLang = await page.locator('html').getAttribute('lang'); - console.log(`页面语言: ${htmlLang || '未设置'}`); - }); -}); diff --git a/e2e/admin-publish-core.spec.ts b/e2e/admin-publish-core.spec.ts deleted file mode 100644 index 78e71b1..0000000 --- a/e2e/admin-publish-core.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -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'; - -test.describe('后台管理发布功能 - 核心测试', () => { - test.beforeEach(async ({ page }) => { - await page.goto(`${BASE_URL}/admin/login`); - await page.waitForLoadState('networkidle'); - - const emailInput = page.locator('#email'); - const passwordInput = page.locator('#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: 15000 }); - }); - - test('管理员登录成功', async ({ page }) => { - expect(page.url()).not.toContain('/admin/login'); - - await page.goto(`${BASE_URL}/admin/content`); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('h1, .text-2xl').first()).toContainText('内容管理'); - }); - - test('后台内容列表加载', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content`); - await page.waitForLoadState('networkidle'); - - const table = page.locator('table'); - await expect(table).toBeVisible(); - - const rows = page.locator('tbody tr'); - const count = await rows.count(); - expect(count).toBeGreaterThanOrEqual(0); - }); - - test('新建内容页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content/new`); - await page.waitForLoadState('domcontentloaded'); - - await page.waitForSelector('input[placeholder="请输入标题"]', { timeout: 60000 }); - await page.waitForSelector('input[placeholder="url-slug"]', { timeout: 60000 }); - - const heading = page.locator('h1, .text-2xl').first(); - await expect(heading).toBeVisible({ timeout: 10000 }); - - const titleInput = page.locator('input[placeholder="请输入标题"]'); - await expect(titleInput).toBeVisible({ timeout: 10000 }); - - const slugInput = page.locator('input[placeholder="url-slug"]'); - await expect(slugInput).toBeVisible({ timeout: 10000 }); - }); - - test('新建内容页面表单元素可见', async ({ page }) => { - await page.goto(`${BASE_URL}/admin/content/new`); - await page.waitForLoadState('domcontentloaded'); - await page.waitForSelector('input[placeholder="请输入标题"]', { timeout: 60000 }); - - const typeSelect = page.locator('select').first(); - await expect(typeSelect).toBeVisible({ timeout: 10000 }); - - const categoryInput = page.locator('input[placeholder="分类名称"]'); - await expect(categoryInput).toBeVisible({ timeout: 10000 }); - - const saveButton = page.locator('button:has-text("保存草稿")'); - await expect(saveButton).toBeVisible({ timeout: 10000 }); - - const publishButton = page.locator('button:has-text("发布")'); - await expect(publishButton).toBeVisible({ timeout: 10000 }); - }); -}); - -test.describe('前端内容展示验证', () => { - test('首页加载正常', async ({ page }) => { - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('header')).toBeVisible(); - await expect(page.locator('footer')).toBeVisible(); - }); - - test('新闻页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/news`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/news/); - await expect(page.locator('header')).toBeVisible(); - }); - - test('产品页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/products`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/products/); - await expect(page.locator('header')).toBeVisible(); - }); - - test('服务页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/services`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/services/); - await expect(page.locator('header')).toBeVisible(); - }); - - test('案例页面加载', async ({ page }) => { - await page.goto(`${BASE_URL}/cases`); - await page.waitForLoadState('networkidle'); - - await expect(page).toHaveURL(/\/cases/); - await expect(page.locator('header')).toBeVisible(); - }); -}); - -test.describe('权限控制测试', () => { - test('未登录访问后台重定向到登录页', async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - - await page.goto(`${BASE_URL}/admin/content`); - await page.waitForURL(/\/admin\/login/, { timeout: 10000 }); - - expect(page.url()).toContain('/admin/login'); - - await context.close(); - }); - - test('API无权限访问返回403', async ({ request }) => { - const response = await request.post(`${BASE_URL}/api/admin/content`, { - data: { - type: 'news', - title: '测试', - slug: 'test', - content: 'test', - }, - }); - - expect([401, 403]).toContain(response.status()); - }); -}); - -test.describe('性能测试', () => { - test('首页加载性能', async ({ page }) => { - const startTime = Date.now(); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - const loadTime = Date.now() - startTime; - - console.log(`首页加载时间: ${loadTime}ms`); - expect(loadTime).toBeLessThan(5000); - }); - - 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(5000); - }); -}); - -test.describe('响应式设计测试', () => { - test('移动端显示', async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }); - await page.goto(BASE_URL); - 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); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('header')).toBeVisible(); - await expect(page.locator('footer')).toBeVisible(); - }); - - test('桌面端显示', async ({ page }) => { - await page.setViewportSize({ width: 1920, height: 1080 }); - await page.goto(BASE_URL); - await page.waitForLoadState('networkidle'); - - await expect(page.locator('header')).toBeVisible(); - await expect(page.locator('footer')).toBeVisible(); - }); -}); diff --git a/e2e/admin-publish.spec.ts b/e2e/admin-publish.spec.ts deleted file mode 100644 index 36c4933..0000000 --- a/e2e/admin-publish.spec.ts +++ /dev/null @@ -1,507 +0,0 @@ -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(); - }); -}); diff --git a/e2e/features/admin/content-crud.spec.ts b/e2e/features/admin/content-crud.spec.ts new file mode 100644 index 0000000..875600a --- /dev/null +++ b/e2e/features/admin/content-crud.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '../../fixtures/auth'; +import { AdminContentPage } from '../../pages'; +import { testFixtures } from '../../fixtures/test-data'; + +test.describe('内容CRUD测试 @feature @admin', () => { + let contentPage: AdminContentPage; + + test.beforeEach(async ({ page }) => { + contentPage = new AdminContentPage(page); + }); + + test('创建新闻内容', async ({ authenticatedPage: _authenticatedPage }) => { + const testNews = testFixtures.testContent.news; + let contentId: string | null = null; + + try { + contentId = await contentPage.createContent(testNews); + expect(contentId).not.toBeNull(); + + await contentPage.expectContentInList(testNews.title); + } finally { + if (contentId) { + await contentPage.deleteContent(contentId); + } + } + }); + + test('创建产品内容', async ({ authenticatedPage: _authenticatedPage }) => { + const testProduct = testFixtures.testContent.product; + let contentId: string | null = null; + + try { + contentId = await contentPage.createContent(testProduct); + expect(contentId).not.toBeNull(); + + await contentPage.expectContentInList(testProduct.title); + } finally { + if (contentId) { + await contentPage.deleteContent(contentId); + } + } + }); + + test('创建内容时验证必填字段', async ({ page, authenticatedPage: _authenticatedPage }) => { + await contentPage.gotoCreate(); + await page.click('button:has-text("发布")'); + + await expect(page.locator('.error-message, [role="alert"]')).toBeVisible(); + }); + + test('删除内容', async ({ authenticatedPage: _authenticatedPage }) => { + const testNews = testFixtures.testContent.news; + const contentId = await contentPage.createContent(testNews); + + if (contentId) { + await contentPage.deleteContent(contentId); + await contentPage.expectContentNotInList(testNews.title); + } + }); +}); diff --git a/e2e/features/admin/user-management.spec.ts b/e2e/features/admin/user-management.spec.ts new file mode 100644 index 0000000..05d6543 --- /dev/null +++ b/e2e/features/admin/user-management.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '../../fixtures/auth'; +import { AdminUserPage } from '../../pages'; + +test.describe('用户管理测试 @feature @admin', () => { + let userPage: AdminUserPage; + + test.beforeEach(async ({ page }) => { + userPage = new AdminUserPage(page); + }); + + test('查看用户列表', async ({ authenticatedPage: _authenticatedPage }) => { + await userPage.goto(); + + const table = userPage['page'].locator('table'); + await expect(table).toBeVisible(); + + const rows = table.locator('tbody tr'); + const count = await rows.count(); + expect(count).toBeGreaterThan(0); + }); + + test('创建新用户', async ({ authenticatedPage: _authenticatedPage }) => { + const timestamp = Date.now(); + const userData = { + email: `test-${timestamp}@example.com`, + password: 'Test123456!', + name: `测试用户${timestamp}`, + role: 'viewer' as const, + }; + + try { + await userPage.createUser(userData); + await userPage.expectUserInList(userData.email); + } finally { + // TODO: 添加删除用户的逻辑 + } + }); + + test('搜索用户', async ({ page, authenticatedPage: _authenticatedPage }) => { + await userPage.goto(); + + const searchInput = page.locator('input[placeholder*="搜索"], input[name="search"]'); + if (await searchInput.count() > 0) { + await searchInput.fill('admin'); + await page.keyboard.press('Enter'); + + const table = page.locator('table'); + await expect(table).toBeVisible(); + } + }); +}); diff --git a/e2e/features/frontend/accessibility.spec.ts b/e2e/features/frontend/accessibility.spec.ts new file mode 100644 index 0000000..a5104cf --- /dev/null +++ b/e2e/features/frontend/accessibility.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test'; + +test.describe('无障碍测试 @feature @frontend', () => { + test('首页无障碍检查', async ({ page }) => { + await page.goto('/'); + + const violations = await page.evaluate(() => { + return (window as unknown as { axe?: { run: () => unknown[] } }).axe?.run() || []; + }); + + expect(violations.length).toBe(0); + }); + + test('导航键盘可访问', async ({ page }) => { + await page.goto('/'); + + await page.keyboard.press('Tab'); + const focusedElement = page.locator(':focus'); + await expect(focusedElement).toBeVisible(); + }); + + test('图片有alt属性', async ({ page }) => { + await page.goto('/'); + + const images = page.locator('img'); + const count = await images.count(); + + for (let i = 0; i < count; i++) { + const img = images.nth(i); + const alt = await img.getAttribute('alt'); + expect(alt).not.toBeNull(); + } + }); + + test('表单标签关联正确', async ({ page }) => { + await page.goto('/contact'); + + const inputs = page.locator('input[type="text"], input[type="email"], textarea'); + const count = await inputs.count(); + + for (let i = 0; i < count; i++) { + const input = inputs.nth(i); + const id = await input.getAttribute('id'); + + if (id) { + const label = page.locator(`label[for="${id}"]`); + const hasLabel = await label.count() > 0; + const hasAriaLabel = await input.getAttribute('aria-label'); + + expect(hasLabel || hasAriaLabel).toBeTruthy(); + } + } + }); + + test('标题层级正确', async ({ page }) => { + await page.goto('/'); + + const h1 = page.locator('h1'); + const h1Count = await h1.count(); + expect(h1Count).toBeGreaterThanOrEqual(1); + expect(h1Count).toBeLessThanOrEqual(1); + }); + + test('链接有明确的文本', async ({ page }) => { + await page.goto('/'); + + const links = page.locator('a'); + const count = await links.count(); + + for (let i = 0; i < Math.min(count, 10); i++) { + const link = links.nth(i); + const text = await link.textContent(); + const ariaLabel = await link.getAttribute('aria-label'); + + expect(text || ariaLabel).toBeTruthy(); + } + }); +}); diff --git a/e2e/features/frontend/responsive.spec.ts b/e2e/features/frontend/responsive.spec.ts new file mode 100644 index 0000000..44a570a --- /dev/null +++ b/e2e/features/frontend/responsive.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +test.describe('响应式测试 @feature @frontend', () => { + test('移动端首页显示正常', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await page.goto('/'); + + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('nav')).toBeVisible(); + await expect(page.locator('footer')).toBeVisible(); + }); + + test('平板端首页显示正常', async ({ page }) => { + await page.setViewportSize({ width: 768, height: 1024 }); + await page.goto('/'); + + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('nav')).toBeVisible(); + }); + + test('桌面端首页显示正常', async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto('/'); + + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('nav')).toBeVisible(); + }); + + test('移动端导航菜单可展开', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await page.goto('/'); + + const menuButton = page.locator('button[aria-label*="菜单"], button[aria-label*="menu"]'); + if (await menuButton.count() > 0) { + await menuButton.click(); + + const mobileMenu = page.locator('[role="dialog"], .mobile-menu, nav[class*="mobile"]'); + await expect(mobileMenu).toBeVisible(); + } + }); +}); diff --git a/e2e/website-acceptance.spec.ts b/e2e/website-acceptance.spec.ts deleted file mode 100644 index f06b6e3..0000000 --- a/e2e/website-acceptance.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('核心功能测试', () => { - test('首页加载正常', async ({ page }) => { - await page.goto('/'); - await expect(page).toHaveTitle(/四川睿新致远科技有限公司/); - await expect(page.locator('header')).toBeVisible(); - await expect(page.locator('footer')).toBeVisible(); - }); - - test('导航功能正常', async ({ page }) => { - await page.goto('/'); - - const navLinks = page.locator('nav a'); - const count = await navLinks.count(); - expect(count).toBeGreaterThan(0); - }); - - test('联系表单显示正常', async ({ page }) => { - await page.goto('/contact'); - - await expect(page.locator('input[name="name"]')).toBeVisible(); - await expect(page.locator('input[name="phone"]')).toBeVisible(); - await expect(page.locator('input[name="email"]')).toBeVisible(); - await expect(page.locator('button[type="submit"]')).toBeVisible(); - }); - - test('ICP备案号显示正确', async ({ page }) => { - await page.goto('/'); - - const footer = page.locator('footer'); - await expect(footer).toContainText('蜀ICP备2026013658号'); - }); -}); diff --git a/package.json b/package.json index 606555d..edeabef 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,13 @@ "test:fast": "TEST_TIER=fast playwright test", "test:standard": "TEST_TIER=standard playwright test", "test:deep": "TEST_TIER=deep playwright test", + "test:smoke": "playwright test --grep @smoke", + "test:journey": "playwright test --grep @journey", + "test:feature": "playwright test --grep @feature", + "test:admin": "playwright test --grep @admin", + "test:frontend": "playwright test --grep @frontend", + "test:ui": "playwright test --ui", + "test:debug": "playwright test --debug", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", diff --git a/playwright.config.ts b/playwright.config.ts index 4ef3c0f..df74cae 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,6 +30,10 @@ const config = tierConfig[testTier]; export default defineConfig({ testDir: './e2e', + testMatch: [ + '**/*.spec.ts', + '**/*.test.ts', + ], fullyParallel: !isCI, forbidOnly: isCI, retries: config.retries,