import { test, expect } from '@playwright/test'; /** * E2E Tests: Shift Sign-Up and Cancellation Flow * * These tests verify the complete shift sign-up workflow including: * - Shift creation by Manager role * - Sign-up and cancellation flow * - Capacity enforcement (full shift blocks sign-up) * - Past shift validation (no sign-up for past shifts) * - Visual capacity indicators (progress bar, spot counts) */ // Helper function to login via Keycloak async function loginAs(page: any, email: string, password: string) { await page.goto('/login'); 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: 15000 }); const isClubPicker = await page.url().includes('/select-club'); if (isClubPicker) { await page.waitForTimeout(1000); const clubCard = page.locator('div.cursor-pointer').first(); await clubCard.click(); await page.waitForURL(/\/dashboard/, { timeout: 10000 }); } } // Helper to logout async function logout(page: any) { await page.click('button:has-text("Sign Out"), button:has-text("Logout")'); await page.waitForURL(/\/login/, { timeout: 10000 }); } // Helper to create a shift async function createShift(page: any, shiftData: { title: string; location?: string; startTime: string; // ISO datetime-local format: "2026-03-10T10:00" endTime: string; capacity: number; }) { await page.goto('/shifts'); await page.click('text=New Shift'); await page.waitForURL('/shifts/new'); await page.locator('label:has-text("Title")').locator('..').locator('input').fill(shiftData.title); if (shiftData.location) { await page.locator('label:has-text("Location")').locator('..').locator('input').fill(shiftData.location); } const startTimeInput = page.locator('input[type="datetime-local"]').first(); await startTimeInput.fill(shiftData.startTime); const endTimeInput = page.locator('input[type="datetime-local"]').nth(1); await endTimeInput.fill(shiftData.endTime); await page.locator('label:has-text("Capacity")').locator('..').locator('input[type="number"]').fill(shiftData.capacity.toString()); await page.click('button:has-text("Create Shift")'); // Wait for redirect to shift detail page await page.waitForURL(/\/shifts\/[0-9a-f-]+/, { timeout: 10000 }); // Extract shift ID from URL const url = page.url(); const shiftId = url.match(/\/shifts\/([0-9a-f-]+)/)[1]; return shiftId; } test.describe('Shift Sign-Up Flow', () => { test.beforeEach(async ({ page }) => { // Ensure clean state await page.goto('/'); }); /** * Scenario 1: Sign up and cancel for shift * * Steps: * 1. Login as manager * 2. Create future shift with capacity 3 * 3. Navigate to shift detail → assert "0/3 spots filled" * 4. Click "Sign Up" → assert "1/3 spots filled" * 5. Assert manager name appears in sign-up list * 6. Click "Cancel Sign-up" → assert "0/3 spots filled" * 7. Assert manager name removed from list * 8. Screenshot → evidence/task-28-shift-signup.png */ test('should allow manager to sign up and cancel for shift', async ({ page }) => { // Login as manager await loginAs(page, 'manager@test.com', 'password'); // Create shift with capacity 3 const futureStartTime = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000); // 2 days from now const futureEndTime = new Date(futureStartTime.getTime() + 2 * 60 * 60 * 1000); // +2 hours const shiftId = await createShift(page, { title: 'Court Maintenance', location: 'Court 3', startTime: futureStartTime.toISOString().slice(0, 16), endTime: futureEndTime.toISOString().slice(0, 16), capacity: 3, }); // Should be on shift detail page now await expect(page).toHaveURL(`/shifts/${shiftId}`); // Assert initial capacity: 0/3 spots filled await expect(page.locator('text=/0\\/3 spots filled/')).toBeVisible(); // Click "Sign Up" button await page.click('button:has-text("Sign Up")'); // Wait for mutation to complete (button text changes or capacity updates) await page.waitForResponse(response => response.url().includes(`/api/shifts/${shiftId}/signup`) && response.status() === 200 ); // Assert capacity updated to 1/3 await expect(page.locator('text=/1\\/3 spots filled/')).toBeVisible(); // Assert "Cancel Sign-up" button appears (replaces "Sign Up") await expect(page.locator('button:has-text("Cancel Sign-up")')).toBeVisible(); // Assert manager appears in sign-up list (Member ID will be visible) await expect(page.locator('text=/Signed Up Members \\(1\\)/')).toBeVisible(); // Screenshot: Sign-up successful await page.screenshot({ path: '.sisyphus/evidence/task-28-shift-signup.png', fullPage: true }); // Click "Cancel Sign-up" await page.click('button:has-text("Cancel Sign-up")'); // Wait for cancel mutation await page.waitForResponse(response => response.url().includes(`/api/shifts/${shiftId}/signup`) && response.status() === 204 ); // Assert capacity back to 0/3 await expect(page.locator('text=/0\\/3 spots filled/')).toBeVisible(); // Assert "Sign Up" button reappears await expect(page.locator('button:has-text("Sign Up")')).toBeVisible(); // Assert sign-up list shows 0 members await expect(page.locator('text=/Signed Up Members \\(0\\)/')).toBeVisible(); await expect(page.locator('text=No sign-ups yet')).toBeVisible(); }); /** * Scenario 2: Full capacity disables sign-up * * Steps: * 1. Login as manager, create shift with capacity 1 * 2. Sign up as manager → capacity "1/1" * 3. Logout, login as member1 * 4. Navigate to same shift → assert "1/1 spots filled" * 5. Assert "Sign Up" button disabled or not present * 6. Screenshot → evidence/task-28-full-capacity.png */ test('should disable sign-up when shift at full capacity', async ({ page }) => { // Login as manager await loginAs(page, 'manager@test.com', 'password'); // Create shift with capacity 1 const futureStartTime = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000); const futureEndTime = new Date(futureStartTime.getTime() + 2 * 60 * 60 * 1000); const shiftId = await createShift(page, { title: 'Small Task', location: 'Office', startTime: futureStartTime.toISOString().slice(0, 16), endTime: futureEndTime.toISOString().slice(0, 16), capacity: 1, }); // Sign up as manager await page.click('button:has-text("Sign Up")'); await page.waitForResponse(response => response.url().includes(`/api/shifts/${shiftId}/signup`) ); // Assert capacity is 1/1 await expect(page.locator('text=/1\\/1 spots filled/')).toBeVisible(); // Logout await logout(page); // Login as member1 await loginAs(page, 'member1@test.com', 'password'); // Navigate to the same shift await page.goto(`/shifts/${shiftId}`); // Assert capacity is 1/1 await expect(page.locator('text=/1\\/1 spots filled/')).toBeVisible(); // Assert "Sign Up" button is not visible (full capacity) const signUpButton = page.locator('button:has-text("Sign Up")'); await expect(signUpButton).not.toBeVisible(); // Screenshot: Full capacity state await page.screenshot({ path: '.sisyphus/evidence/task-28-full-capacity.png', fullPage: true }); }); /** * Scenario 3: Past shift has no sign-up button * * Steps: * 1. Login as manager * 2. Create shift with startTime in past (e.g., yesterday) * 3. Navigate to shift detail * 4. Assert "Past" badge visible * 5. Assert "Sign Up" button not rendered * 6. Verify past shifts still display correctly (read-only) */ test('should not allow sign-up for past shifts', async ({ page }) => { // Login as manager await loginAs(page, 'manager@test.com', 'password'); // Create shift in the past (yesterday) const pastStartTime = new Date(Date.now() - 24 * 60 * 60 * 1000); // 1 day ago const pastEndTime = new Date(pastStartTime.getTime() + 2 * 60 * 60 * 1000); const shiftId = await createShift(page, { title: 'Past Shift', location: 'Court 1', startTime: pastStartTime.toISOString().slice(0, 16), endTime: pastEndTime.toISOString().slice(0, 16), capacity: 5, }); // Should be on shift detail page await expect(page).toHaveURL(`/shifts/${shiftId}`); // Assert "Past" badge is visible await expect(page.locator('text=Past')).toBeVisible(); // Assert "Sign Up" button is NOT rendered const signUpButton = page.locator('button:has-text("Sign Up")'); await expect(signUpButton).not.toBeVisible(); // Assert shift details are still readable await expect(page.locator('text=Past Shift')).toBeVisible(); await expect(page.locator('text=/0\\/5 spots filled/')).toBeVisible(); // Assert capacity progress bar is still visible (read-only display) await expect(page.locator('span:has-text("Capacity")')).toBeVisible(); }); /** * Scenario 4: Progress bar updates correctly * * Verifies that the capacity progress bar accurately reflects sign-up count. */ test('should update progress bar as sign-ups increase', async ({ page }) => { // Login as manager await loginAs(page, 'manager@test.com', 'password'); // Create shift with capacity 2 const futureStartTime = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000); const futureEndTime = new Date(futureStartTime.getTime() + 2 * 60 * 60 * 1000); const shiftId = await createShift(page, { title: 'Progress Test', location: 'Gym', startTime: futureStartTime.toISOString().slice(0, 16), endTime: futureEndTime.toISOString().slice(0, 16), capacity: 2, }); // Assert initial state: 0/2 await expect(page.locator('text=/0\\/2 spots filled/')).toBeVisible(); // Sign up as manager await page.click('button:has-text("Sign Up")'); await page.waitForResponse(response => response.url().includes(`/api/shifts/${shiftId}/signup`) ); // Assert: 1/2 filled (50% progress) await expect(page.locator('text=/1\\/2 spots filled/')).toBeVisible(); // Progress bar should be visible and partially filled const progressBar = page.locator('[role="progressbar"]'); await expect(progressBar).toBeVisible(); // Logout and login as member1 await logout(page); await loginAs(page, 'member1@test.com', 'password'); // Navigate to shift and sign up await page.goto(`/shifts/${shiftId}`); await page.click('button:has-text("Sign Up")'); await page.waitForResponse(response => response.url().includes(`/api/shifts/${shiftId}/signup`) ); // Assert: 2/2 filled (100% progress, full capacity) await expect(page.locator('text=/2\\/2 spots filled/')).toBeVisible(); // Sign Up button should no longer be visible const signUpButton = page.locator('button:has-text("Sign Up")'); await expect(signUpButton).not.toBeVisible(); }); });