import { test, expect, Page } from '@playwright/test'; /** * E2E Tests for Task Management Flow * * Covers: * - Full task lifecycle (Create → Open → Assigned → InProgress → Review → Done) * - Role-based access (Manager can create, Viewer cannot) * - Status filtering * - State transitions following domain rules */ // Test user credentials from Task 3 const USERS = { admin: { email: 'admin@test.com', password: 'testpass123' }, manager: { email: 'manager@test.com', password: 'testpass123' }, viewer: { email: 'viewer@test.com', password: 'testpass123' }, }; // Helper function to login via Keycloak async function loginAs(page: Page, email: string, password: string) { await page.goto('/'); // Check if already logged in const isLoggedIn = await page.locator('text=Tasks').isVisible().catch(() => false); if (isLoggedIn) { // Logout first await page.goto('/api/auth/signout'); await page.locator('button:has-text("Sign out")').click().catch(() => {}); await page.waitForTimeout(1000); } // Navigate to login await page.goto('/login'); await page.waitForLoadState('networkidle'); // Fill Keycloak login form await page.fill('input[name="username"]', email); await page.fill('input[name="password"]', password); await page.click('input[type="submit"]'); // Wait for redirect back to app await page.waitForURL(/\/(dashboard|tasks)/, { timeout: 10000 }); } // Helper to select club (assumes club-1 as default tenant) async function selectClub(page: Page, clubName: string = 'club-1') { // Check if club selector is visible const clubSelector = page.locator('select[name="club"], button:has-text("Club")').first(); if (await clubSelector.isVisible().catch(() => false)) { await clubSelector.click(); await page.locator(`text=${clubName}`).first().click(); await page.waitForTimeout(500); // Wait for localStorage update } } test.describe('Task Management E2E', () => { test.beforeEach(async ({ page }) => { // Clear localStorage to start fresh await page.goto('/'); await page.evaluate(() => localStorage.clear()); }); test('Scenario 1: Full task lifecycle via UI', async ({ page }) => { // Step 1: Login as admin await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); // Step 2: Navigate to /tasks await page.goto('/tasks'); await page.waitForLoadState('networkidle'); // Step 3: Click "New Task" button const newTaskButton = page.locator('text=New Task'); await expect(newTaskButton).toBeVisible(); await newTaskButton.click(); // Step 4: Fill form await page.waitForURL(/\/tasks\/new/); await page.fill('input[name="title"]', 'Replace court net'); await page.fill('textarea[name="description"]', 'Net on court 3 is torn'); // Step 5: Submit form await page.locator('button:has-text("Create Task")').click(); // Step 6: Assert redirected to task detail await page.waitForURL(/\/tasks\/[0-9a-f-]+/, { timeout: 10000 }); // Step 7: Assert status badge shows "Open" const statusBadge = page.locator('text=Open').first(); await expect(statusBadge).toBeVisible(); // Verify task title await expect(page.locator('text=Replace court net')).toBeVisible(); // Step 8: Transition Open → Assigned const assignButton = page.locator('button:has-text("Move to Assigned")'); await expect(assignButton).toBeVisible(); await assignButton.click(); await page.waitForTimeout(1000); // Wait for mutation await expect(page.locator('text=Assigned').first()).toBeVisible(); // Step 9: Transition Assigned → InProgress const startButton = page.locator('button:has-text("Move to InProgress")'); await expect(startButton).toBeVisible(); await startButton.click(); await page.waitForTimeout(1000); await expect(page.locator('text=InProgress').first()).toBeVisible(); // Step 10: Transition InProgress → Review const reviewButton = page.locator('button:has-text("Move to Review")'); await expect(reviewButton).toBeVisible(); await reviewButton.click(); await page.waitForTimeout(1000); await expect(page.locator('text=Review').first()).toBeVisible(); // Step 11: Transition Review → Done const doneButton = page.locator('button:has-text("Mark as Done")'); await expect(doneButton).toBeVisible(); await doneButton.click(); await page.waitForTimeout(1000); await expect(page.locator('text=Done').first()).toBeVisible(); // Step 12: Screenshot final state await page.screenshot({ path: '.sisyphus/evidence/task-27-task-lifecycle.png', fullPage: true }); }); test('Scenario 2: Viewer cannot create tasks', async ({ page }) => { // Step 1: Login as viewer await loginAs(page, USERS.viewer.email, USERS.viewer.password); await selectClub(page, 'club-1'); // Step 2: Navigate to /tasks await page.goto('/tasks'); await page.waitForLoadState('networkidle'); // Step 3: Assert "New Task" button is NOT visible const newTaskButton = page.locator('text=New Task'); await expect(newTaskButton).not.toBeVisible(); // Step 4: Assert can see task list (read permission) const tasksTable = page.locator('table'); await expect(tasksTable).toBeVisible(); // Step 5: Screenshot await page.screenshot({ path: '.sisyphus/evidence/task-27-viewer-no-create.png', fullPage: true }); }); test('Scenario 3: Task list filters by status', async ({ page }) => { // Step 1: Login as admin await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); // Step 2: Navigate to /tasks await page.goto('/tasks'); await page.waitForLoadState('networkidle'); // Step 3: Open status filter dropdown const statusFilter = page.locator('select').first(); await expect(statusFilter).toBeVisible(); // Step 4: Select "Open" status await statusFilter.selectOption('Open'); await page.waitForTimeout(1000); // Wait for filter to apply // Step 5: Verify only Open tasks displayed const statusBadges = page.locator('table tbody tr td:nth-child(2)'); const badgeCount = await statusBadges.count(); if (badgeCount > 0) { // Verify all visible badges say "Open" for (let i = 0; i < badgeCount; i++) { const badgeText = await statusBadges.nth(i).textContent(); expect(badgeText?.trim()).toBe('Open'); } } // Step 6: Select "Done" status await statusFilter.selectOption('Done'); await page.waitForTimeout(1000); // Step 7: Verify only Done tasks displayed const doneBadgeCount = await statusBadges.count(); if (doneBadgeCount > 0) { for (let i = 0; i < doneBadgeCount; i++) { const badgeText = await statusBadges.nth(i).textContent(); expect(badgeText?.trim()).toBe('Done'); } } // Step 8: Clear filter (select "All Statuses") await statusFilter.selectOption(''); await page.waitForTimeout(1000); // Step 9: Verify all tasks displayed (mixed statuses) const allBadgeCount = await statusBadges.count(); expect(allBadgeCount).toBeGreaterThanOrEqual(0); }); test('State transition validation: Cannot skip states', async ({ page }) => { // Login as admin and create a task await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); await page.goto('/tasks/new'); await page.fill('input[name="title"]', 'Test state validation'); await page.locator('button:has-text("Create Task")').click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); // Verify task is Open await expect(page.locator('text=Open').first()).toBeVisible(); // Valid transition: Open → Assigned should be available await expect(page.locator('button:has-text("Move to Assigned")')).toBeVisible(); // Invalid transitions should NOT be available await expect(page.locator('button:has-text("Move to InProgress")')).not.toBeVisible(); await expect(page.locator('button:has-text("Move to Review")')).not.toBeVisible(); await expect(page.locator('button:has-text("Mark as Done")')).not.toBeVisible(); }); test('Review can transition back to InProgress', async ({ page }) => { // Login as admin and create a task await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); await page.goto('/tasks/new'); await page.fill('input[name="title"]', 'Test review back to progress'); await page.locator('button:has-text("Create Task")').click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); // Transition through states to Review await page.locator('button:has-text("Move to Assigned")').click(); await page.waitForTimeout(1000); await page.locator('button:has-text("Move to InProgress")').click(); await page.waitForTimeout(1000); await page.locator('button:has-text("Move to Review")').click(); await page.waitForTimeout(1000); // Verify in Review state await expect(page.locator('text=Review').first()).toBeVisible(); // Verify both forward (Done) and backward (InProgress) transitions available await expect(page.locator('button:has-text("Mark as Done")')).toBeVisible(); await expect(page.locator('button:has-text("Back to InProgress")')).toBeVisible(); // Test backward transition await page.locator('button:has-text("Back to InProgress")').click(); await page.waitForTimeout(1000); await expect(page.locator('text=InProgress').first()).toBeVisible(); }); test('Manager can create and update tasks', async ({ page }) => { // Login as manager (not admin) await loginAs(page, USERS.manager.email, USERS.manager.password); await selectClub(page, 'club-1'); // Navigate to tasks await page.goto('/tasks'); // Verify "New Task" button is visible for Manager await expect(page.locator('text=New Task')).toBeVisible(); // Create a task await page.locator('text=New Task').click(); await page.fill('input[name="title"]', 'Manager created task'); await page.locator('button:has-text("Create Task")').click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); // Verify task created successfully await expect(page.locator('text=Manager created task')).toBeVisible(); await expect(page.locator('text=Open').first()).toBeVisible(); // Verify can transition states await expect(page.locator('button:has-text("Move to Assigned")')).toBeVisible(); }); test('Task detail page shows all task information', async ({ page }) => { // Login and create a task with all fields await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); await page.goto('/tasks/new'); await page.fill('input[name="title"]', 'Complete task details test'); await page.fill('textarea[name="description"]', 'This task has all fields filled'); // Set due date (tomorrow) const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const dueDateStr = tomorrow.toISOString().split('T')[0]; await page.fill('input[name="dueDate"]', dueDateStr); await page.locator('button:has-text("Create Task")').click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); // Verify all fields are displayed await expect(page.locator('text=Complete task details test')).toBeVisible(); await expect(page.locator('text=This task has all fields filled')).toBeVisible(); await expect(page.locator('text=Open').first()).toBeVisible(); await expect(page.locator('text=Unassigned')).toBeVisible(); await expect(page.locator('text=Created At')).toBeVisible(); await expect(page.locator('text=Due Date')).toBeVisible(); }); test('Pagination controls work correctly', async ({ page }) => { // Login as admin await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); // Navigate to tasks await page.goto('/tasks'); await page.waitForLoadState('networkidle'); // Check if pagination controls are visible (only if more than pageSize tasks) const paginationControls = page.locator('text=Page'); const hasPagination = await paginationControls.isVisible().catch(() => false); if (hasPagination) { // Verify "Previous" button is disabled on page 1 const prevButton = page.locator('button:has-text("Previous")'); await expect(prevButton).toBeDisabled(); // Click "Next" button const nextButton = page.locator('button:has-text("Next")'); await nextButton.click(); await page.waitForTimeout(1000); // Verify page number increased await expect(page.locator('text=Page 2')).toBeVisible(); // Verify "Previous" button is now enabled await expect(prevButton).toBeEnabled(); } }); test('Back button navigation from task detail', async ({ page }) => { // Login and navigate to tasks await loginAs(page, USERS.admin.email, USERS.admin.password); await selectClub(page, 'club-1'); await page.goto('/tasks/new'); await page.fill('input[name="title"]', 'Test back navigation'); await page.locator('button:has-text("Create Task")').click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); // Click "Back to Tasks" link const backLink = page.locator('text=← Back to Tasks'); await expect(backLink).toBeVisible(); await backLink.click(); // Verify navigated back to task list await page.waitForURL(/\/tasks$/); await expect(page.locator('text=Tasks').first()).toBeVisible(); }); });