# 摄影作品集网站 - 测试需求文档 ## 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(); // 验证网格布局 const grid = screen.getByTestId('photo-grid'); expect(grid).toBeInTheDocument(); // 验证图片数量 const images = screen.getAllByRole('img'); expect(images).toHaveLength(mockPhotos.length); }); test('应该支持懒加载', async () => { const { container } = render(); // 只有可视区域的图片应该被加载 const loadedImages = container.querySelectorAll('img[src]'); expect(loadedImages.length).toBeLessThan(longPhotoList.length); }); test('应该响应式适配不同屏幕', () => { // 桌面端 Object.defineProperty(window, 'innerWidth', { value: 1200 }); render(); expect(screen.getByTestId('photo-grid')).toHaveClass('lg:grid-cols-4'); // 移动端 Object.defineProperty(window, 'innerWidth', { value: 600 }); render(); 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(); const yearButton = screen.getByText('2023'); fireEvent.click(yearButton); await waitFor(() => { expect(screen.getByTestId('timeline-content')).toHaveTextContent('2023'); }); }); test('应该显示正确的照片数量统计', () => { render(); const monthSummary = screen.getByTestId('month-summary-1'); expect(monthSummary).toHaveTextContent('15张照片'); }); }); ``` ### 2.3 收藏夹功能测试 ```javascript describe('收藏夹功能', () => { test('应该能创建新收藏夹', async () => { render(); 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(); 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(); 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(); 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: ['/src/setupTests.js'], moduleNameMapping: { '^@/(.*)$': '/src/$1', '^@/components/(.*)$': '/src/components/$1' }, testMatch: [ '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', '/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" } } ```