feat(frontend): implement activity feed component (#43)
Add shared ActivityFeed component for real-time project activity: - Real-time connection indicator (Live, Connecting, Disconnected, Error) - Time-based event grouping (Today, Yesterday, This Week, Older) - Event type filtering with category checkboxes - Search functionality for filtering events - Expandable event details with raw payload view - Approval request handling (approve/reject buttons) - Loading skeleton and empty state handling - Compact mode for dashboard embedding - WCAG AA accessibility (keyboard navigation, ARIA labels) Components: - ActivityFeed.tsx: Main shared component (900+ lines) - Activity page at /activity for full-page view - Demo events when SSE not connected Testing: - 45 unit tests covering all features - E2E tests for page functionality Closes #43 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
202
frontend/e2e/activity-feed.spec.ts
Normal file
202
frontend/e2e/activity-feed.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* E2E Tests for Activity Feed Page
|
||||
*
|
||||
* Tests the real-time activity feed functionality:
|
||||
* - Page navigation and layout
|
||||
* - Event display and filtering
|
||||
* - Search functionality
|
||||
* - Approval handling
|
||||
* - Time-based grouping
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Activity Feed Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to activity page (authenticated route)
|
||||
// The page uses demo mode when SSE is not connected
|
||||
await page.goto('/en/activity');
|
||||
});
|
||||
|
||||
test('displays page header with title', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Activity Feed', level: 1 })).toBeVisible();
|
||||
await expect(page.getByText('Real-time updates from your projects')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows demo mode banner when not connected to SSE', async ({ page }) => {
|
||||
// Demo mode banner should be visible
|
||||
await expect(page.getByText(/Demo Mode/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing sample events/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('displays activity feed component', async ({ page }) => {
|
||||
await expect(page.getByTestId('activity-feed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('displays demo events in time groups', async ({ page }) => {
|
||||
// Should have time-based grouping
|
||||
await expect(page.getByTestId('event-group-today')).toBeVisible();
|
||||
});
|
||||
|
||||
test('search functionality filters events', async ({ page }) => {
|
||||
const searchInput = page.getByTestId('search-input');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// Search for a specific term
|
||||
await searchInput.fill('JWT');
|
||||
|
||||
// Should find the JWT-related event
|
||||
await expect(page.getByText(/Completed JWT/)).toBeVisible();
|
||||
|
||||
// Clear search
|
||||
await searchInput.clear();
|
||||
});
|
||||
|
||||
test('filter panel toggles visibility', async ({ page }) => {
|
||||
const filterToggle = page.getByTestId('filter-toggle');
|
||||
await expect(filterToggle).toBeVisible();
|
||||
|
||||
// Click to open filter panel
|
||||
await filterToggle.click();
|
||||
await expect(page.getByTestId('filter-panel')).toBeVisible();
|
||||
|
||||
// Click to close filter panel
|
||||
await filterToggle.click();
|
||||
await expect(page.getByTestId('filter-panel')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('filter by event category', async ({ page }) => {
|
||||
// Open filter panel
|
||||
await page.getByTestId('filter-toggle').click();
|
||||
|
||||
// Select Agent Actions filter
|
||||
await page.getByLabel(/Agent Actions/).click();
|
||||
|
||||
// Should filter events
|
||||
// Agent events should still be visible
|
||||
await expect(page.getByText(/Completed JWT/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('pending approvals filter', async ({ page }) => {
|
||||
// Open filter panel
|
||||
await page.getByTestId('filter-toggle').click();
|
||||
|
||||
// Select pending only filter
|
||||
await page.getByLabel(/Show only pending approvals/).click();
|
||||
|
||||
// Only approval events should be visible
|
||||
await expect(page.getByText(/Approval required/)).toBeVisible();
|
||||
await expect(page.getByText(/Completed JWT/)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('event item can be expanded', async ({ page }) => {
|
||||
// Find and click an event item
|
||||
const firstEvent = page.getByTestId(/event-item-/).first();
|
||||
await firstEvent.click();
|
||||
|
||||
// Event details should be visible
|
||||
await expect(page.getByTestId('event-details')).toBeVisible();
|
||||
|
||||
// Should show raw payload option
|
||||
await expect(page.getByText('View raw payload')).toBeVisible();
|
||||
});
|
||||
|
||||
test('approval actions are visible for pending approvals', async ({ page }) => {
|
||||
// Find approval event
|
||||
const approvalEvent = page.locator('[data-testid^="event-item-"]', {
|
||||
has: page.getByText('Action Required'),
|
||||
}).first();
|
||||
|
||||
// Approval buttons should be visible
|
||||
await expect(approvalEvent.getByTestId('approve-button')).toBeVisible();
|
||||
await expect(approvalEvent.getByTestId('reject-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('notification toggle works', async ({ page }) => {
|
||||
const bellButton = page.getByLabel(/notifications/i);
|
||||
await expect(bellButton).toBeVisible();
|
||||
|
||||
// Click to toggle notifications
|
||||
await bellButton.click();
|
||||
|
||||
// Bell icon should change (hard to verify icon change in E2E, but click should work)
|
||||
await expect(bellButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('refresh button triggers reconnection', async ({ page }) => {
|
||||
const refreshButton = page.getByLabel('Refresh connection');
|
||||
await expect(refreshButton).toBeVisible();
|
||||
|
||||
// Click refresh
|
||||
await refreshButton.click();
|
||||
|
||||
// Button should still be visible and enabled
|
||||
await expect(refreshButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('pending count badge shows correct count', async ({ page }) => {
|
||||
// Should show pending count
|
||||
await expect(page.getByText(/pending$/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('keyboard navigation works for events', async ({ page }) => {
|
||||
// Tab to first event
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Find focused element and press Enter
|
||||
const focusedElement = page.locator(':focus');
|
||||
|
||||
// If it's an event item, pressing Enter should expand it
|
||||
const testId = await focusedElement.getAttribute('data-testid');
|
||||
if (testId?.startsWith('event-item-')) {
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.getByTestId('event-details')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('clear filters button works', async ({ page }) => {
|
||||
// Open filter panel and set some filters
|
||||
await page.getByTestId('filter-toggle').click();
|
||||
await page.getByLabel(/Agent Actions/).click();
|
||||
|
||||
// Click clear filters
|
||||
await page.getByText('Clear Filters').click();
|
||||
|
||||
// All events should be visible again
|
||||
await expect(page.getByText(/Completed JWT/)).toBeVisible();
|
||||
await expect(page.getByText(/Approval required/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('responsive layout adapts to viewport', async ({ page }) => {
|
||||
// Test mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Page should still be functional
|
||||
await expect(page.getByTestId('activity-feed')).toBeVisible();
|
||||
await expect(page.getByTestId('search-input')).toBeVisible();
|
||||
|
||||
// Reset viewport
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Activity Feed - Authenticated Routes', () => {
|
||||
test('redirects to login when not authenticated', async ({ page }) => {
|
||||
// Clear any existing auth state
|
||||
await page.context().clearCookies();
|
||||
|
||||
// Try to access activity page
|
||||
await page.goto('/en/activity');
|
||||
|
||||
// Should redirect to login (AuthGuard behavior)
|
||||
// The exact behavior depends on AuthGuard implementation
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Either on activity page (if demo mode) or redirected to login
|
||||
const url = page.url();
|
||||
expect(url.includes('/activity') || url.includes('/login')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user