ebaa7f3c50
ci/woodpecker/manual/woodpecker Pipeline was successful
- 移除未使用的YAML锚点定义 - 替换commands字段中的锚点引用为实际值 - 移除有问题的通知步骤 - 修复测试文件中的问题 - 添加新的测试用例和配置文件
508 lines
16 KiB
TypeScript
508 lines
16 KiB
TypeScript
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: '<p>这是测试新闻的正文内容</p><p>包含多个段落</p>',
|
|
category: '公司新闻',
|
|
tags: ['测试', '自动化'],
|
|
status: 'published',
|
|
},
|
|
{
|
|
type: 'product',
|
|
title: `测试产品-${Date.now()}`,
|
|
slug: `test-product-${Date.now()}`,
|
|
excerpt: '这是一个测试产品的描述',
|
|
content: '<p>测试产品的详细介绍</p>',
|
|
category: '软件产品',
|
|
tags: ['产品', '测试'],
|
|
status: 'published',
|
|
},
|
|
{
|
|
type: 'service',
|
|
title: `测试服务-${Date.now()}`,
|
|
slug: `test-service-${Date.now()}`,
|
|
excerpt: '这是一个测试服务的描述',
|
|
content: '<p>测试服务的详细介绍</p>',
|
|
category: '软件开发',
|
|
tags: ['服务', '测试'],
|
|
status: 'published',
|
|
},
|
|
{
|
|
type: 'case',
|
|
title: `测试案例-${Date.now()}`,
|
|
slug: `test-case-${Date.now()}`,
|
|
excerpt: '这是一个测试案例的描述',
|
|
content: '<p>测试案例的详细介绍</p>',
|
|
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<string | null> {
|
|
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: '<p>草稿内容</p>',
|
|
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: '<script>alert("XSS")</script>测试摘要',
|
|
content: '<p><script>alert("XSS")</script>测试内容</p>',
|
|
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();
|
|
});
|
|
});
|