grabbit 46d8af6084 🎉 Epic 2 Milestone: Successfully completed the final story of Epic 2: Commercialization & Core User Experience with full-stack date filtering functionality.
📋 What Was Accomplished

  Backend Changes:
  -  Enhanced API Endpoint: Updated GET /api/v1/events to accept optional date query parameter
  -  Input Validation: Added YYYY-MM-DD format validation to PaginationQueryDto
  -  Database Filtering: Implemented timezone-aware date filtering in EventsService
  -  Controller Integration: Updated EventsController to pass date parameter to service

  Frontend Changes:
  -  Date Picker Component: Created reusable DatePicker component following project design system
  -  Gallery UI Enhancement: Integrated date picker into gallery page with clear labeling
  -  State Management: Implemented reactive date state with automatic re-fetching
  -  Clear Filter Functionality: Added "Clear Filter" button for easy reset
  -  Enhanced UX: Improved empty states for filtered vs unfiltered views

  🔍 Technical Implementation

  API Design:
  GET /api/v1/events?date=2025-08-02&limit=20&cursor=xxx

  Key Files Modified:
  - meteor-web-backend/src/events/dto/pagination-query.dto.ts
  - meteor-web-backend/src/events/events.service.ts
  - meteor-web-backend/src/events/events.controller.ts
  - meteor-frontend/src/components/ui/date-picker.tsx (new)
  - meteor-frontend/src/app/gallery/page.tsx
  - meteor-frontend/src/hooks/use-events.ts
  - meteor-frontend/src/services/events.ts

   All Acceptance Criteria Met

  1.  Backend API Enhancement: Accepts optional date parameter
  2.  Date Filtering Logic: Returns events for specific calendar date
  3.  Date Picker UI: Clean, accessible interface component
  4.  Automatic Re-fetching: Immediate data updates on date selection
  5.  Filtered Display: Correctly shows only events for selected date
  6.  Clear Filter: One-click reset to view all events

  🧪 Quality Assurance

  -  Backend Build: Successful compilation with no errors
  -  Frontend Build: Successful Next.js build with no warnings
  -  Linting: All ESLint checks pass
  -  Functionality: Feature working as specified

  🎉 Epic 2 Complete!

  With Story 2.9 completion, Epic 2: Commercialization & Core User Experience is now DONE!

  Epic 2 Achievements:
  - 🔐 Full-stack device status monitoring
  - 💳 Robust payment and subscription system
  - 🛡️ Subscription-based access control
  - 📊 Enhanced data browsing with detail pages
  - 📅 Date-based event filtering
2025-08-03 10:30:29 +08:00

258 lines
8.0 KiB
TypeScript
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.

import { test, expect } from '@playwright/test';
test.describe('Gallery Page E2E Tests', () => {
const testUser = {
email: 'e2e-test@meteor.dev',
password: 'TestPassword123',
displayName: 'E2E Test User'
};
test.beforeEach(async ({ page }) => {
// 导航到首页
await page.goto('/');
});
test('should redirect to login when accessing gallery without authentication', async ({ page }) => {
// 直接访问gallery页面
await page.goto('/gallery');
// 应该被重定向到登录页面
await expect(page).toHaveURL('/login');
// 检查登录页面元素
await expect(page.locator('h1')).toContainText('登录');
});
test('should display gallery page after successful login', async ({ page }) => {
// 先注册用户
await page.goto('/register');
await page.fill('input[name="email"]', testUser.email);
await page.fill('input[name="password"]', testUser.password);
await page.fill('input[name="displayName"]', testUser.displayName);
await page.click('button[type="submit"]');
// 等待重定向到dashboard
await expect(page).toHaveURL('/dashboard');
// 访问gallery页面
await page.goto('/gallery');
// 检查gallery页面元素
await expect(page.locator('h1')).toContainText('Event Gallery');
// 检查空状态消息
await expect(page.locator('text=No events captured yet')).toBeVisible();
});
test('should display loading state initially', async ({ page }) => {
// 登录用户
await loginUser(page, testUser);
// 访问gallery页面
await page.goto('/gallery');
// 检查loading状态可能很快消失所以用waitFor
const loadingText = page.locator('text=Loading events...');
if (await loadingText.isVisible()) {
await expect(loadingText).toBeVisible();
}
// 最终应该显示空状态或事件列表
await expect(page.locator('h1')).toContainText('Event Gallery');
});
test('should handle API errors gracefully', async ({ page }) => {
// 模拟API错误
await page.route('**/api/v1/events**', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server Error' })
});
});
// 登录用户
await loginUser(page, testUser);
// 访问gallery页面
await page.goto('/gallery');
// 检查错误消息
await expect(page.locator('text=Error loading events')).toBeVisible();
});
test('should display events when API returns data', async ({ page }) => {
// 模拟API返回测试数据
const mockEvents = {
data: [
{
id: 'test-event-1',
deviceId: 'device-1',
eventType: 'meteor',
capturedAt: '2024-01-01T12:00:00Z',
mediaUrl: 'https://example.com/test1.jpg',
isValid: true,
createdAt: '2024-01-01T12:00:00Z',
metadata: { location: 'Test Location 1' }
},
{
id: 'test-event-2',
deviceId: 'device-1',
eventType: 'satellite',
capturedAt: '2024-01-01T11:00:00Z',
mediaUrl: 'https://example.com/test2.jpg',
isValid: true,
createdAt: '2024-01-01T11:00:00Z',
metadata: { location: 'Test Location 2' }
}
],
nextCursor: null
};
await page.route('**/api/v1/events**', route => {
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mockEvents)
});
});
// 登录用户
await loginUser(page, testUser);
// 访问gallery页面
await page.goto('/gallery');
// 检查事件卡片
await expect(page.locator('[data-testid="event-card"]').first()).toBeVisible();
await expect(page.locator('text=Type: meteor')).toBeVisible();
await expect(page.locator('text=Type: satellite')).toBeVisible();
await expect(page.locator('text=Location: Test Location 1')).toBeVisible();
await expect(page.locator('text=Location: Test Location 2')).toBeVisible();
});
test('should implement infinite scroll', async ({ page }) => {
// 模拟分页数据
let pageCount = 0;
await page.route('**/api/v1/events**', route => {
const url = new URL(route.request().url());
const cursor = url.searchParams.get('cursor');
if (!cursor) {
// 第一页
pageCount = 1;
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: Array.from({ length: 5 }, (_, i) => ({
id: `event-page1-${i}`,
deviceId: 'device-1',
eventType: 'meteor',
capturedAt: new Date(Date.now() - i * 60000).toISOString(),
mediaUrl: `https://example.com/page1-${i}.jpg`,
isValid: true,
createdAt: new Date().toISOString()
})),
nextCursor: 'page2-cursor'
})
});
} else if (cursor === 'page2-cursor') {
// 第二页
pageCount = 2;
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: Array.from({ length: 3 }, (_, i) => ({
id: `event-page2-${i}`,
deviceId: 'device-1',
eventType: 'satellite',
capturedAt: new Date(Date.now() - (i + 5) * 60000).toISOString(),
mediaUrl: `https://example.com/page2-${i}.jpg`,
isValid: true,
createdAt: new Date().toISOString()
})),
nextCursor: null
})
});
}
});
// 登录用户
await loginUser(page, testUser);
// 访问gallery页面
await page.goto('/gallery');
// 等待第一页加载
await expect(page.locator('[data-testid="event-card"]')).toHaveCount(5);
// 滚动到底部触发infinite scroll
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// 等待第二页加载
await expect(page.locator('[data-testid="event-card"]')).toHaveCount(8);
// 检查loading more状态
const loadingMore = page.locator('text=Loading more events...');
if (await loadingMore.isVisible()) {
await expect(loadingMore).toBeVisible();
}
});
test('should be responsive on mobile devices', async ({ page }) => {
// 设置移动设备视口
await page.setViewportSize({ width: 375, height: 667 });
// 模拟事件数据
await page.route('**/api/v1/events**', route => {
route.fulfill({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [{
id: 'mobile-test-event',
deviceId: 'device-1',
eventType: 'meteor',
capturedAt: '2024-01-01T12:00:00Z',
mediaUrl: 'https://example.com/mobile-test.jpg',
isValid: true,
createdAt: '2024-01-01T12:00:00Z'
}],
nextCursor: null
})
});
});
// 登录用户
await loginUser(page, testUser);
// 访问gallery页面
await page.goto('/gallery');
// 检查移动端布局
await expect(page.locator('h1')).toContainText('Event Gallery');
await expect(page.locator('[data-testid="event-card"]')).toBeVisible();
// 检查网格布局在移动端的响应性
const grid = page.locator('[data-testid="gallery-grid"]');
if (await grid.isVisible()) {
const gridColumns = await grid.evaluate(el =>
window.getComputedStyle(el).gridTemplateColumns
);
// 移动端应该是单列
expect(gridColumns).toContain('1fr');
}
});
// 辅助函数:登录用户
async function loginUser(page: any, user: typeof testUser) {
await page.goto('/login');
await page.fill('input[name="email"]', user.email);
await page.fill('input[name="password"]', user.password);
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
}
});