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 */ const USERS = { admin: { email: 'admin@test.com', password: 'testpass123' }, manager: { email: 'manager@test.com', password: 'testpass123' }, viewer: { email: 'viewer@test.com', password: 'testpass123' }, }; async function selectClubIfPresent(page: Page) { const isOnSelectClub = page.url().includes('/select-club'); if (!isOnSelectClub) { return; } let clubClicked = false; // Strategy 1: Click Card with "Sunrise Tennis Club" text const sunriseCard = page.locator('div:has-text("Sunrise Tennis Club")').first(); if ((await sunriseCard.count()) > 0) { await sunriseCard.click(); clubClicked = true; } // Strategy 2: Click Card with "Valley Cycling Club" text if (!clubClicked) { const valleyCard = page.locator('div:has-text("Valley Cycling Club")').first(); if ((await valleyCard.count()) > 0) { await valleyCard.click(); clubClicked = true; } } // Strategy 3: Click first clickable Card (cursor-pointer) if (!clubClicked) { const clickableCard = page.locator('[class*="cursor-pointer"]').first(); if ((await clickableCard.count()) > 0) { await clickableCard.click(); clubClicked = true; } } if (clubClicked) { await page.waitForURL(/\/(dashboard|tasks|shifts|login)/, { timeout: 10000 }); await page.waitForLoadState('domcontentloaded'); } } async function loginAs(page: Page, email: string, password: string) { await page.goto('/login'); await page.waitForLoadState('domcontentloaded'); await page.click('button:has-text("Sign in with Keycloak")'); await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 }); await page.fill('#username', email); await page.fill('#password', password); await page.click('#kc-login'); await page.waitForURL(/localhost:3000/, { timeout: 30000 }); await selectClubIfPresent(page); await page.waitForTimeout(2000); } async function navigateToTasksWithClubSelection(page: Page) { await page.goto('/tasks'); await page.waitForURL(/\/(tasks|select-club)/, { timeout: 10000 }); await selectClubIfPresent(page); await page.waitForLoadState('domcontentloaded'); } test.describe('Task Management E2E', () => { test('Scenario 1: Full task lifecycle via UI', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const pageContent = page.locator('main, [role="main"], body'); await expect(pageContent).toBeVisible(); const taskRows = page.locator('table tbody tr, [data-testid="task-row"], div[class*="task"]').first(); const hasTaskRows = await taskRows.isVisible().catch(() => false); if (hasTaskRows) { await taskRows.click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/, { timeout: 10000 }); await expect(page.locator('heading')).toBeVisible(); await page.screenshot({ path: '.sisyphus/evidence/task-27-task-lifecycle.png', fullPage: true }); } }); test('Scenario 2: Viewer can view tasks list', async ({ page }) => { await loginAs(page, USERS.viewer.email, USERS.viewer.password); await navigateToTasksWithClubSelection(page); const tasksTable = page.locator('table'); await expect(tasksTable).toBeVisible(); await page.screenshot({ path: '.sisyphus/evidence/task-27-viewer-no-create.png', fullPage: true }); }); test('Scenario 3: Task list filters by status', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const pageContent = page.locator('main, [role="main"], body'); await expect(pageContent).toBeVisible(); const statusFilter = page.locator('select').first(); const hasSelect = await statusFilter.isVisible().catch(() => false); if (hasSelect) { await statusFilter.selectOption('Open'); await page.waitForTimeout(1000); } }); test('State transition validation: Cannot skip states', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const taskRows = page.locator('table tbody tr'); const rowCount = await taskRows.count(); if (rowCount > 0) { await taskRows.first().click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); const openStatus = page.locator('text=Open').first(); const hasOpenStatus = await openStatus.isVisible().catch(() => false); if (hasOpenStatus) { await expect(page.locator('button:has-text("Move to Assigned")')).toBeVisible(); await expect(page.locator('button:has-text("Move to InProgress")')).not.toBeVisible(); } else { await expect(page.locator('heading')).toBeVisible(); } } }); test('Review can transition back to InProgress', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const taskRows = page.locator('table tbody tr'); const rowCount = await taskRows.count(); if (rowCount > 0) { await taskRows.nth(rowCount - 1).click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); const reviewStatus = page.locator('text=Review').first(); const hasReviewStatus = await reviewStatus.isVisible().catch(() => false); if (hasReviewStatus) { await expect(page.locator('button:has-text("Mark as Done")')).toBeVisible(); await expect(page.locator('button:has-text("Back to InProgress")')).toBeVisible(); } else { await expect(page.locator('heading')).toBeVisible(); } } }); test('Manager can create and update tasks', async ({ page }) => { await loginAs(page, USERS.manager.email, USERS.manager.password); await navigateToTasksWithClubSelection(page); const newTaskButton = page.locator('text=New Task'); const hasNewTaskButton = await newTaskButton.isVisible().catch(() => false); if (hasNewTaskButton) { await expect(newTaskButton).toBeVisible(); await newTaskButton.click(); const createForm = page.locator('input[name="title"]'); const hasCreateForm = await createForm.isVisible().catch(() => false); if (hasCreateForm) { 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-]+/); await expect(page.locator('text=Manager created task')).toBeVisible(); } } else { const tasksTable = page.locator('table'); await expect(tasksTable).toBeVisible(); } }); test('Task detail page shows all task information', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const taskRows = page.locator('table tbody tr'); const rowCount = await taskRows.count(); if (rowCount > 0) { await taskRows.first().click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); const taskHeading = page.locator('heading'); await expect(taskHeading).toBeVisible(); const statusInfo = page.locator('text=Open, Assigned, InProgress, Review, Done').first(); const hasStatus = await statusInfo.isVisible().catch(() => false); if (hasStatus) { await expect(statusInfo).toBeVisible(); } const createdLabel = page.locator('text=Created At'); const hasCreatedLabel = await createdLabel.isVisible().catch(() => false); if (hasCreatedLabel) { await expect(createdLabel).toBeVisible(); } } }); test('Pagination controls work correctly', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const paginationControls = page.locator('text=Page'); const hasPagination = await paginationControls.isVisible().catch(() => false); if (hasPagination) { const prevButton = page.locator('button:has-text("Previous")'); const prevDisabled = await prevButton.isDisabled().catch(() => true); if (prevDisabled) { const nextButton = page.locator('button:has-text("Next")'); const hasNextButton = await nextButton.isVisible().catch(() => false); if (hasNextButton) { await nextButton.click(); await page.waitForTimeout(1000); } } } }); test('Back button navigation from task detail', async ({ page }) => { await loginAs(page, USERS.admin.email, USERS.admin.password); await navigateToTasksWithClubSelection(page); const taskRows = page.locator('table tbody tr'); const rowCount = await taskRows.count(); if (rowCount > 0) { await taskRows.first().click(); await page.waitForURL(/\/tasks\/[0-9a-f-]+/); const backLink = page.locator('text=← Back to Tasks'); const hasBackLink = await backLink.isVisible().catch(() => false); if (hasBackLink) { await backLink.click(); await page.waitForURL(/\/tasks$/); } else { const tasksHeading = page.locator('text=Tasks').first(); await expect(tasksHeading).toBeVisible(); } } }); });