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

15 KiB
Raw Blame History

摄影作品集网站 - 测试需求文档

1. 测试策略概述

1.1 测试目标

  • 功能完整性:确保所有功能按需求正常工作
  • 性能稳定性:验证系统在各种负载下的表现
  • 用户体验:保证界面交互流畅自然
  • 兼容性:确保跨设备、跨浏览器的一致性
  • 安全性:验证数据安全和用户隐私保护

1.2 测试类型分布

  • 单元测试40%组件、工具函数、API端点
  • 集成测试30%(模块间交互、数据流)
  • 端到端测试20%(用户完整流程)
  • 性能测试10%(加载速度、响应时间)

2. 功能测试用例

2.1 图片管理功能

2.1.1 图片上传测试

// 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 图片展示测试

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 时间线功能测试

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 收藏夹功能测试

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 图片加载性能测试

// 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响应时间测试

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 浏览器兼容性测试

// 使用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 设备响应式测试

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 交互流程测试

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 可访问性测试

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配置

// 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配置

// 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集成

// 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 负载测试

// 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配置

# .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 测试覆盖率报告

// package.json scripts
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:e2e": "playwright test",
    "lighthouse:ci": "lhci autorun"
  }
}