Files
photography/docs/原始 prd/测试需求文档.md
xujiang 9e381c783d
All checks were successful
Deploy Frontend / deploy (push) Successful in 2m38s
feat: 重构项目为模块化结构,拆分 CLAUDE.md 文档
## 📁 模块化重构

### 新增模块 CLAUDE.md
- `frontend/CLAUDE.md` - 前端开发指导 (Next.js, React, TypeScript)
- `docs/deployment/CLAUDE.md` - 部署配置指导 (Caddy, 服务器配置)
- `.gitea/workflows/CLAUDE.md` - CI/CD 流程指导 (Gitea Actions)

### 根目录 CLAUDE.md 优化
- 重构为项目概览和模块导航
- 提供模块选择指导
- 减少单个文件的上下文长度

### 自动化机制
- 创建 `scripts/update-claude-docs.sh` 自动更新脚本
- 集成到 pre-commit hooks 中
- 文件变更时自动更新对应模块的 CLAUDE.md

## 🎯 优化效果

### 上下文优化
- 每个模块独立的 CLAUDE.md 文件
- 大幅减少单次处理的上下文长度
- 提高 Claude 处理效率和准确性

### 开发体验
- 根据工作内容选择对应模块
- 模块化的文档更聚焦和专业
- 自动维护文档时间戳

### 项目结构
```
photography/
├── CLAUDE.md                    # 项目概览和模块导航
├── frontend/CLAUDE.md          # 前端开发指导
├── docs/deployment/CLAUDE.md   # 部署配置指导
├── .gitea/workflows/CLAUDE.md  # CI/CD 流程指导
└── scripts/update-claude-docs.sh # 自动更新脚本
```

现在 Claude 工作时只需关注单个模块的文档,大幅提升处理效率!
2025-07-09 10:54:08 +08:00

562 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 摄影作品集网站 - 测试需求文档
## 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"
}
}
```