/** * E2E Tests for Projects List Page * * Tests the projects CRUD page showing: * - Page header with title and create button * - Search and filter controls * - Grid/list view toggle * - Project cards with status and progress * - Filtering and sorting functionality * - Navigation to project details * * @module e2e/projects-list.spec * @see Issue #54 */ import { test, expect } from '@playwright/test'; import { setupAuthenticatedMocks, loginViaUI } from './helpers/auth'; test.describe('Projects List Page', () => { test.beforeEach(async ({ page }) => { await setupAuthenticatedMocks(page); }); test('should display page header with title', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Check page title await expect(page.getByRole('heading', { level: 1, name: /Projects/i })).toBeVisible(); await expect(page.getByText('Manage your projects')).toBeVisible(); }); test('should display Create Project button', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); const createButton = page.getByRole('link', { name: /Create Project/i }); await expect(createButton).toBeVisible(); await expect(createButton).toHaveAttribute('href', /\/projects\/new/); }); test('should display search input', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); const searchInput = page.getByPlaceholder(/Search projects/i); await expect(searchInput).toBeVisible(); }); test('should display status filter dropdown', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); const statusFilter = page.getByRole('combobox', { name: /Filter by status/i }); await expect(statusFilter).toBeVisible(); }); test('should display view mode toggle buttons', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); await expect(page.getByRole('button', { name: /Grid view/i })).toBeVisible(); await expect(page.getByRole('button', { name: /List view/i })).toBeVisible(); }); test('should display project cards in grid by default', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Grid should have grid layout classes const gridContainer = page.locator('.grid').first(); await expect(gridContainer).toBeVisible(); }); test('should toggle to list view when clicking list button', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Click list view button await page.getByRole('button', { name: /List view/i }).click(); // Should switch to list layout (space-y class) const listContainer = page.locator('.space-y-4').first(); const count = await listContainer.count(); expect(count).toBeGreaterThanOrEqual(0); }); test('should filter projects by search query', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Type in search const searchInput = page.getByPlaceholder(/Search projects/i); await searchInput.fill('test query'); // Wait for debounce and filter await page.waitForTimeout(500); // Search should be applied (URL or state change) await expect(searchInput).toHaveValue('test query'); }); test('should show Filters button and expand filters on click', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Click Filters button const filtersButton = page.getByRole('button', { name: /Filters/i }); await expect(filtersButton).toBeVisible(); await filtersButton.click(); // Extended filters should be visible await expect(page.getByText('Complexity')).toBeVisible(); await expect(page.getByText('Sort By')).toBeVisible(); }); test('should be accessible from dashboard', async ({ page }) => { await loginViaUI(page); await page.waitForLoadState('networkidle'); // Click "View all" link from dashboard const viewAllLink = page.getByRole('link', { name: /View all/i }).first(); await viewAllLink.click(); // Should be on projects page await expect(page).toHaveURL(/\/projects/); await expect(page.getByRole('heading', { level: 1, name: /Projects/i })).toBeVisible(); }); test('should have accessible heading hierarchy', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Check for h1 (page title) await expect(page.locator('h1')).toBeVisible(); }); test('should be keyboard navigable', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Tab through the page elements await page.keyboard.press('Tab'); // Should be able to focus on interactive elements const focusedElement = page.locator(':focus'); await expect(focusedElement).toBeVisible(); }); test('should show responsive layout on mobile', async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }); await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Page should still be functional on mobile await expect(page.getByRole('heading', { level: 1, name: /Projects/i })).toBeVisible(); await expect(page.getByPlaceholder(/Search projects/i)).toBeVisible(); }); test('should load within acceptable time', async ({ page }) => { await loginViaUI(page); // Measure navigation timing const start = Date.now(); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); const duration = Date.now() - start; // Projects page should load within 5 seconds expect(duration).toBeLessThan(5000); }); }); test.describe('Projects List Project Cards', () => { test.beforeEach(async ({ page }) => { await setupAuthenticatedMocks(page); }); test('should display project cards with status badges', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Check for status badges (Active, Paused, etc.) const statusBadges = page.locator('[class*="badge"]'); const count = await statusBadges.count(); expect(count).toBeGreaterThan(0); }); test('should display progress bars on project cards', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Check for progress bars const progressBars = page.getByRole('progressbar'); const count = await progressBars.count(); expect(count).toBeGreaterThanOrEqual(0); }); test('should navigate to project detail on card click', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Click first project card const projectCards = page.locator('[role="button"]'); const cardCount = await projectCards.count(); if (cardCount > 0) { await projectCards.first().click(); // Should navigate to project detail await expect(page).toHaveURL(/\/projects\//); } }); }); test.describe('Projects List Filtering', () => { test.beforeEach(async ({ page }) => { await setupAuthenticatedMocks(page); }); test('should filter by status', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Open status filter const statusFilter = page.getByRole('combobox', { name: /Filter by status/i }); await statusFilter.click(); // Select "Active" status const activeOption = page.getByRole('option', { name: /Active/i }); const count = await activeOption.count(); if (count > 0) { await activeOption.click(); } }); test('should show active filter count badge', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Open status filter and select a value const statusFilter = page.getByRole('combobox', { name: /Filter by status/i }); await statusFilter.click(); const activeOption = page.getByRole('option', { name: /Active/i }); const count = await activeOption.count(); if (count > 0) { await activeOption.click(); // Filter badge should show count const filterBadge = page.locator('.rounded-full').filter({ hasText: /\d/ }); const badgeCount = await filterBadge.count(); expect(badgeCount).toBeGreaterThanOrEqual(0); } }); test('should clear filters when clicking Clear Filters', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Fill search to create a filter const searchInput = page.getByPlaceholder(/Search projects/i); await searchInput.fill('test'); await page.waitForTimeout(500); // Open extended filters const filtersButton = page.getByRole('button', { name: /Filters/i }); await filtersButton.click(); // Click Clear Filters if visible const clearButton = page.getByRole('button', { name: /Clear Filters/i }); const count = await clearButton.count(); if (count > 0) { await clearButton.click(); await expect(searchInput).toHaveValue(''); } }); }); test.describe('Projects List Empty State', () => { test.beforeEach(async ({ page }) => { await setupAuthenticatedMocks(page); }); test('should show appropriate message when no projects match filter', async ({ page }) => { await loginViaUI(page); await page.goto('/en/projects'); await page.waitForLoadState('networkidle'); // Search for something that won't match const searchInput = page.getByPlaceholder(/Search projects/i); await searchInput.fill('xyznonexistent123'); await page.waitForTimeout(500); // Should show empty state message const noResultsText = page.getByText(/No projects found/i); const count = await noResultsText.count(); // In demo mode, filter might still return results; empty state tested at component level expect(count).toBeGreaterThanOrEqual(0); }); });