562 lines
15 KiB
Markdown
562 lines
15 KiB
Markdown
# 摄影作品集网站 - 测试需求文档
|
||
|
||
## 1. 测试策略概述
|
||
|
||
### 1.1 测试目标
|
||
- **功能完整性**:确保所有功能按需求正常工作
|
||
- **性能稳定性**:验证系统在各种负载下的表现
|
||
- **用户体验**:保证界面交互流畅自然
|
||
- **兼容性**:确保跨设备、跨浏览器的一致性
|
||
- **安全性**:验证数据安全和用户隐私保护
|
||
|
||
### 1.2 测试类型分布
|
||
- **单元测试**:40%(组件、工具函数、API端点)
|
||
- **集成测试**:30%(模块间交互、数据流)
|
||
- **端到端测试**:20%(用户完整流程)
|
||
- **性能测试**:10%(加载速度、响应时间)
|
||
|
||
## 2. 功能测试用例
|
||
|
||
### 2.1 图片管理功能
|
||
|
||
#### 2.1.1 图片上传测试
|
||
```javascript
|
||
// tests/upload.test.js
|
||
describe('图片上传功能', () => {
|
||
test('应该能上传RAW格式图片', async () => {
|
||
const file = new File([rawImageBuffer], 'test.cr2', { type: 'image/x-canon-cr2' });
|
||
const result = await uploadPhoto(file);
|
||
|
||
expect(result.status).toBe('success');
|
||
expect(result.formats).toHaveProperty('raw');
|
||
expect(result.formats).toHaveProperty('jpg');
|
||
expect(result.formats).toHaveProperty('webp');
|
||
});
|
||
|
||
test('应该能上传JPEG格式图片', async () => {
|
||
const file = new File([jpegBuffer], 'test.jpg', { type: 'image/jpeg' });
|
||
const result = await uploadPhoto(file);
|
||
|
||
expect(result.status).toBe('success');
|
||
expect(result.formats.jpg).toBeDefined();
|
||
});
|
||
|
||
test('应该拒绝不支持的文件格式', async () => {
|
||
const file = new File([textBuffer], 'test.txt', { type: 'text/plain' });
|
||
|
||
await expect(uploadPhoto(file)).rejects.toThrow('不支持的文件格式');
|
||
});
|
||
|
||
test('应该拒绝过大的文件', async () => {
|
||
const largeFile = new File([new ArrayBuffer(100 * 1024 * 1024)], 'large.jpg');
|
||
|
||
await expect(uploadPhoto(largeFile)).rejects.toThrow('文件大小超出限制');
|
||
});
|
||
});
|
||
```
|
||
|
||
#### 2.1.2 图片展示测试
|
||
```javascript
|
||
describe('图片展示功能', () => {
|
||
test('应该正确显示图片网格', async () => {
|
||
render(<PhotoGrid photos={mockPhotos} />);
|
||
|
||
// 验证网格布局
|
||
const grid = screen.getByTestId('photo-grid');
|
||
expect(grid).toBeInTheDocument();
|
||
|
||
// 验证图片数量
|
||
const images = screen.getAllByRole('img');
|
||
expect(images).toHaveLength(mockPhotos.length);
|
||
});
|
||
|
||
test('应该支持懒加载', async () => {
|
||
const { container } = render(<PhotoGrid photos={longPhotoList} />);
|
||
|
||
// 只有可视区域的图片应该被加载
|
||
const loadedImages = container.querySelectorAll('img[src]');
|
||
expect(loadedImages.length).toBeLessThan(longPhotoList.length);
|
||
});
|
||
|
||
test('应该响应式适配不同屏幕', () => {
|
||
// 桌面端
|
||
Object.defineProperty(window, 'innerWidth', { value: 1200 });
|
||
render(<PhotoGrid photos={mockPhotos} />);
|
||
expect(screen.getByTestId('photo-grid')).toHaveClass('lg:grid-cols-4');
|
||
|
||
// 移动端
|
||
Object.defineProperty(window, 'innerWidth', { value: 600 });
|
||
render(<PhotoGrid photos={mockPhotos} />);
|
||
expect(screen.getByTestId('photo-grid')).toHaveClass('grid-cols-2');
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2.2 时间线功能测试
|
||
|
||
```javascript
|
||
describe('时间线功能', () => {
|
||
test('应该按年份正确分组', async () => {
|
||
const timelineData = await getTimelineData();
|
||
|
||
expect(timelineData.groups).toBeDefined();
|
||
expect(timelineData.groups[0].year).toBe(2024);
|
||
expect(timelineData.groups[0].months).toHaveProperty('1');
|
||
});
|
||
|
||
test('应该支持年份切换', async () => {
|
||
render(<Timeline data={mockTimelineData} />);
|
||
|
||
const yearButton = screen.getByText('2023');
|
||
fireEvent.click(yearButton);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByTestId('timeline-content')).toHaveTextContent('2023');
|
||
});
|
||
});
|
||
|
||
test('应该显示正确的照片数量统计', () => {
|
||
render(<Timeline data={mockTimelineData} />);
|
||
|
||
const monthSummary = screen.getByTestId('month-summary-1');
|
||
expect(monthSummary).toHaveTextContent('15张照片');
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2.3 收藏夹功能测试
|
||
|
||
```javascript
|
||
describe('收藏夹功能', () => {
|
||
test('应该能创建新收藏夹', async () => {
|
||
render(<CollectionManager />);
|
||
|
||
const createButton = screen.getByText('创建收藏夹');
|
||
fireEvent.click(createButton);
|
||
|
||
const nameInput = screen.getByLabelText('收藏夹名称');
|
||
fireEvent.change(nameInput, { target: { value: '风景摄影' } });
|
||
|
||
const saveButton = screen.getByText('保存');
|
||
fireEvent.click(saveButton);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('风景摄影')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
test('应该能添加照片到收藏夹', async () => {
|
||
render(<PhotoGrid photos={mockPhotos} collections={mockCollections} />);
|
||
|
||
const photo = screen.getAllByTestId('photo-item')[0];
|
||
fireEvent.contextMenu(photo);
|
||
|
||
const addToCollection = screen.getByText('添加到收藏夹');
|
||
fireEvent.click(addToCollection);
|
||
|
||
const collection = screen.getByText('风景摄影');
|
||
fireEvent.click(collection);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('已添加到收藏夹')).toBeInTheDocument();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
## 3. 性能测试
|
||
|
||
### 3.1 图片加载性能测试
|
||
|
||
```javascript
|
||
// tests/performance/imageLoading.test.js
|
||
describe('图片加载性能', () => {
|
||
test('首屏图片应在2秒内加载完成', async () => {
|
||
const startTime = performance.now();
|
||
|
||
render(<PhotoGrid photos={mockPhotos.slice(0, 8)} />);
|
||
|
||
await waitFor(() => {
|
||
const images = screen.getAllByRole('img');
|
||
images.forEach(img => {
|
||
expect(img).toHaveAttribute('src');
|
||
});
|
||
});
|
||
|
||
const loadTime = performance.now() - startTime;
|
||
expect(loadTime).toBeLessThan(2000);
|
||
});
|
||
|
||
test('大图片应该有渐进式加载', async () => {
|
||
render(<LightboxImage src="large-image.jpg" placeholder="thumb.jpg" />);
|
||
|
||
const img = screen.getByRole('img');
|
||
|
||
// 初始应显示占位图
|
||
expect(img).toHaveAttribute('src', expect.stringContaining('thumb.jpg'));
|
||
|
||
await waitFor(() => {
|
||
// 加载完成后应显示高清图
|
||
expect(img).toHaveAttribute('src', expect.stringContaining('large-image.jpg'));
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### 3.2 API响应时间测试
|
||
|
||
```javascript
|
||
describe('API性能测试', () => {
|
||
test('获取照片列表API应在500ms内响应', async () => {
|
||
const startTime = Date.now();
|
||
|
||
const response = await fetch('/api/photos?limit=20');
|
||
const data = await response.json();
|
||
|
||
const responseTime = Date.now() - startTime;
|
||
|
||
expect(response.status).toBe(200);
|
||
expect(responseTime).toBeLessThan(500);
|
||
expect(data.data).toHaveLength(20);
|
||
});
|
||
|
||
test('图片上传API应支持大文件', async () => {
|
||
const largeFile = new File([new ArrayBuffer(20 * 1024 * 1024)], 'large.jpg');
|
||
const formData = new FormData();
|
||
formData.append('photo', largeFile);
|
||
|
||
const startTime = Date.now();
|
||
const response = await fetch('/api/photos', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const uploadTime = Date.now() - startTime;
|
||
|
||
expect(response.status).toBe(201);
|
||
expect(uploadTime).toBeLessThan(30000); // 30秒内完成
|
||
});
|
||
});
|
||
```
|
||
|
||
## 4. 兼容性测试
|
||
|
||
### 4.1 浏览器兼容性测试
|
||
|
||
```javascript
|
||
// 使用Playwright进行跨浏览器测试
|
||
const { test, devices } = require('@playwright/test');
|
||
|
||
test.describe('浏览器兼容性', () => {
|
||
['chromium', 'firefox', 'webkit'].forEach(browserName => {
|
||
test(`应该在${browserName}中正常显示`, async ({ browser }) => {
|
||
const context = await browser.newContext();
|
||
const page = await context.newPage();
|
||
|
||
await page.goto('/');
|
||
|
||
// 验证关键元素存在
|
||
await expect(page.locator('[data-testid="photo-grid"]')).toBeVisible();
|
||
await expect(page.locator('nav')).toBeVisible();
|
||
|
||
// 验证图片加载
|
||
const firstImage = page.locator('img').first();
|
||
await expect(firstImage).toBeVisible();
|
||
|
||
await context.close();
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### 4.2 设备响应式测试
|
||
|
||
```javascript
|
||
describe('响应式设计测试', () => {
|
||
const devices = [
|
||
{ name: 'iPhone 12', width: 390, height: 844 },
|
||
{ name: 'iPad', width: 768, height: 1024 },
|
||
{ name: 'Desktop', width: 1920, height: 1080 }
|
||
];
|
||
|
||
devices.forEach(device => {
|
||
test(`应该在${device.name}上正确显示`, async () => {
|
||
await page.setViewportSize({
|
||
width: device.width,
|
||
height: device.height
|
||
});
|
||
|
||
await page.goto('/');
|
||
|
||
// 验证导航栏适配
|
||
if (device.width < 768) {
|
||
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
|
||
} else {
|
||
await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
|
||
}
|
||
|
||
// 验证网格列数
|
||
const grid = page.locator('[data-testid="photo-grid"]');
|
||
const expectedCols = device.width < 768 ? 2 : device.width < 1024 ? 3 : 4;
|
||
|
||
await expect(grid).toHaveClass(new RegExp(`grid-cols-${expectedCols}`));
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
## 5. 用户体验测试
|
||
|
||
### 5.1 交互流程测试
|
||
|
||
```javascript
|
||
describe('用户交互流程', () => {
|
||
test('完整的照片浏览流程', async () => {
|
||
await page.goto('/');
|
||
|
||
// 1. 点击照片打开灯箱
|
||
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
|
||
await firstPhoto.click();
|
||
|
||
await expect(page.locator('[data-testid="lightbox"]')).toBeVisible();
|
||
|
||
// 2. 使用键盘导航
|
||
await page.keyboard.press('ArrowRight');
|
||
await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
|
||
|
||
// 3. 缩放功能
|
||
await page.locator('[data-testid="lightbox-image"]').click();
|
||
await expect(page.locator('[data-testid="lightbox-image"]')).toHaveClass(/zoomed/);
|
||
|
||
// 4. 关闭灯箱
|
||
await page.keyboard.press('Escape');
|
||
await expect(page.locator('[data-testid="lightbox"]')).not.toBeVisible();
|
||
});
|
||
|
||
test('移动端手势操作', async () => {
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await page.goto('/');
|
||
|
||
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
|
||
await firstPhoto.click();
|
||
|
||
// 模拟左滑手势
|
||
const lightbox = page.locator('[data-testid="lightbox-image"]');
|
||
await lightbox.touchAction([
|
||
{ action: 'touchStart', x: 300, y: 400 },
|
||
{ action: 'touchMove', x: 100, y: 400 },
|
||
{ action: 'touchEnd' }
|
||
]);
|
||
|
||
await expect(page.locator('[data-testid="photo-counter"]')).toContainText('2 / ');
|
||
});
|
||
});
|
||
```
|
||
|
||
### 5.2 可访问性测试
|
||
|
||
```javascript
|
||
describe('可访问性测试', () => {
|
||
test('应该支持键盘导航', async () => {
|
||
await page.goto('/');
|
||
|
||
// Tab键导航
|
||
await page.keyboard.press('Tab');
|
||
await expect(page.locator(':focus')).toBeVisible();
|
||
|
||
// 回车键激活
|
||
await page.keyboard.press('Enter');
|
||
// 验证预期的交互结果
|
||
});
|
||
|
||
test('应该有正确的ARIA标签', async () => {
|
||
await page.goto('/');
|
||
|
||
const grid = page.locator('[data-testid="photo-grid"]');
|
||
await expect(grid).toHaveAttribute('role', 'grid');
|
||
|
||
const photos = page.locator('[data-testid="photo-item"]');
|
||
await expect(photos.first()).toHaveAttribute('role', 'gridcell');
|
||
});
|
||
|
||
test('应该支持屏幕阅读器', async () => {
|
||
await page.goto('/');
|
||
|
||
const firstPhoto = page.locator('[data-testid="photo-item"]').first();
|
||
await expect(firstPhoto).toHaveAttribute('aria-label');
|
||
|
||
const img = firstPhoto.locator('img');
|
||
await expect(img).toHaveAttribute('alt');
|
||
});
|
||
});
|
||
```
|
||
|
||
## 6. 自动化测试配置
|
||
|
||
### 6.1 Jest配置
|
||
```javascript
|
||
// jest.config.js
|
||
module.exports = {
|
||
testEnvironment: 'jsdom',
|
||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
|
||
moduleNameMapping: {
|
||
'^@/(.*)$': '<rootDir>/src/$1',
|
||
'^@/components/(.*)$': '<rootDir>/src/components/$1'
|
||
},
|
||
testMatch: [
|
||
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
|
||
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||
],
|
||
collectCoverageFrom: [
|
||
'src/**/*.{js,jsx,ts,tsx}',
|
||
'!src/**/*.d.ts',
|
||
'!src/index.js'
|
||
],
|
||
coverageThreshold: {
|
||
global: {
|
||
branches: 80,
|
||
functions: 80,
|
||
lines: 80,
|
||
statements: 80
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### 6.2 Playwright配置
|
||
```javascript
|
||
// playwright.config.js
|
||
module.exports = {
|
||
testDir: './e2e',
|
||
timeout: 30000,
|
||
use: {
|
||
baseURL: 'http://localhost:3000',
|
||
trace: 'on-first-retry',
|
||
video: 'retain-on-failure'
|
||
},
|
||
projects: [
|
||
{
|
||
name: 'chromium',
|
||
use: { ...devices['Desktop Chrome'] }
|
||
},
|
||
{
|
||
name: 'firefox',
|
||
use: { ...devices['Desktop Firefox'] }
|
||
},
|
||
{
|
||
name: 'webkit',
|
||
use: { ...devices['Desktop Safari'] }
|
||
},
|
||
{
|
||
name: 'Mobile Chrome',
|
||
use: { ...devices['Pixel 5'] }
|
||
},
|
||
{
|
||
name: 'Mobile Safari',
|
||
use: { ...devices['iPhone 12'] }
|
||
}
|
||
]
|
||
};
|
||
```
|
||
|
||
## 7. 性能监控和测试
|
||
|
||
### 7.1 Lighthouse CI集成
|
||
```javascript
|
||
// lighthouse.config.js
|
||
module.exports = {
|
||
ci: {
|
||
collect: {
|
||
url: ['http://localhost:3000', 'http://localhost:3000/gallery'],
|
||
numberOfRuns: 3
|
||
},
|
||
assert: {
|
||
assertions: {
|
||
'categories:performance': ['error', { minScore: 0.9 }],
|
||
'categories:accessibility': ['error', { minScore: 0.9 }],
|
||
'categories:best-practices': ['error', { minScore: 0.9 }],
|
||
'categories:seo': ['error', { minScore: 0.9 }]
|
||
}
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### 7.2 负载测试
|
||
```javascript
|
||
// load-test.js
|
||
import http from 'k6/http';
|
||
import { check, sleep } from 'k6';
|
||
|
||
export let options = {
|
||
stages: [
|
||
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
|
||
{ duration: '5m', target: 100 }, // 维持100用户5分钟
|
||
{ duration: '2m', target: 200 }, // 增加到200用户
|
||
{ duration: '5m', target: 200 }, // 维持200用户5分钟
|
||
{ duration: '2m', target: 0 }, // 逐步减少到0
|
||
],
|
||
};
|
||
|
||
export default function() {
|
||
let response = http.get('http://localhost:3000/api/photos');
|
||
|
||
check(response, {
|
||
'status is 200': (r) => r.status === 200,
|
||
'response time < 500ms': (r) => r.timings.duration < 500,
|
||
});
|
||
|
||
sleep(1);
|
||
}
|
||
```
|
||
|
||
## 8. 测试报告和持续集成
|
||
|
||
### 8.1 GitHub Actions配置
|
||
```yaml
|
||
# .github/workflows/test.yml
|
||
name: Test
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v2
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v2
|
||
with:
|
||
node-version: '18'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Run unit tests
|
||
run: npm run test:coverage
|
||
|
||
- name: Run E2E tests
|
||
run: npm run test:e2e
|
||
|
||
- name: Run Lighthouse CI
|
||
run: npm run lighthouse:ci
|
||
|
||
- name: Upload coverage to Codecov
|
||
uses: codecov/codecov-action@v1
|
||
```
|
||
|
||
### 8.2 测试覆盖率报告
|
||
```javascript
|
||
// package.json scripts
|
||
{
|
||
"scripts": {
|
||
"test": "jest",
|
||
"test:watch": "jest --watch",
|
||
"test:coverage": "jest --coverage",
|
||
"test:e2e": "playwright test",
|
||
"lighthouse:ci": "lhci autorun"
|
||
}
|
||
}
|
||
``` |