2026-03-05 10:34:03 +01:00
|
|
|
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)
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-06 22:26:55 +01:00
|
|
|
async function selectClubIfPresent(page: import('@playwright/test').Page) {
|
2026-03-06 16:03:03 +01:00
|
|
|
const isOnSelectClub = page.url().includes('/select-club');
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
if (!isOnSelectClub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
let clubClicked = false;
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
if (clubClicked) {
|
|
|
|
|
await page.waitForURL(/\/(dashboard|tasks|shifts|login)/, { timeout: 10000 });
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
2026-03-05 10:34:03 +01:00
|
|
|
}
|
2026-03-06 16:03:03 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:26:55 +01:00
|
|
|
async function loginAs(page: import('@playwright/test').Page, email: string, password: string) {
|
2026-03-06 16:03:03 +01:00
|
|
|
await page.goto('/login');
|
|
|
|
|
await page.click('button:has-text("Sign in with Keycloak")');
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 });
|
|
|
|
|
await page.fill('#username', email);
|
|
|
|
|
await page.fill('#password', password);
|
|
|
|
|
await page.click('#kc-login');
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
await page.waitForURL(/localhost:3000/, { timeout: 30000 });
|
2026-03-05 10:34:03 +01:00
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
await selectClubIfPresent(page);
|
2026-03-05 10:34:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.describe('Shift Sign-Up Flow', () => {
|
2026-03-06 16:03:03 +01:00
|
|
|
test('should allow sign up and cancel on existing shifts', async ({ page }) => {
|
|
|
|
|
await loginAs(page, 'manager@test.com', 'testpass123');
|
|
|
|
|
|
|
|
|
|
await page.goto('/shifts');
|
|
|
|
|
await page.waitForURL(/\/(shifts|select-club)/, { timeout: 10000 });
|
|
|
|
|
await selectClubIfPresent(page);
|
|
|
|
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
|
|
const signUpButtons = page.locator('button:has-text("Sign Up")');
|
|
|
|
|
const hasSignUpButton = (await signUpButtons.count()) > 0;
|
|
|
|
|
|
|
|
|
|
if (hasSignUpButton) {
|
|
|
|
|
const firstSignUpButton = signUpButtons.first();
|
|
|
|
|
|
|
|
|
|
await firstSignUpButton.click();
|
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
|
|
|
|
|
|
const cancelButton = page.locator('button:has-text("Cancel Sign-up")').first();
|
|
|
|
|
await expect(cancelButton).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await cancelButton.click();
|
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
|
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: '.sisyphus/evidence/task-28-shift-signup.png',
|
|
|
|
|
fullPage: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('✅ Shift sign-up and cancel completed');
|
|
|
|
|
} else {
|
|
|
|
|
const shiftsGrid = page.locator('[class*="grid"], table').first();
|
|
|
|
|
await expect(shiftsGrid).toBeVisible();
|
|
|
|
|
console.log('✅ Shifts list visible (no sign-up scenario)');
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
});
|
|
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
test('should not allow sign-up when shift at full capacity', async ({ page }) => {
|
|
|
|
|
await loginAs(page, 'manager@test.com', 'testpass123');
|
|
|
|
|
|
|
|
|
|
await page.goto('/shifts');
|
|
|
|
|
await page.waitForURL(/\/(shifts|select-club)/, { timeout: 10000 });
|
|
|
|
|
await selectClubIfPresent(page);
|
|
|
|
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
|
|
const shiftsGrid = page.locator('[class*="grid"], table').first();
|
|
|
|
|
await expect(shiftsGrid).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const noSignUpButtons = page.locator('div:not(:has(button:has-text("Sign Up")))').filter({ hasText: /\/.*spots filled/ });
|
|
|
|
|
const hasFullShift = (await noSignUpButtons.count()) > 0;
|
|
|
|
|
|
|
|
|
|
if (hasFullShift) {
|
|
|
|
|
const fullShiftCard = noSignUpButtons.first();
|
|
|
|
|
const capacityText = await fullShiftCard.textContent();
|
|
|
|
|
expect(capacityText).toContain('spots filled');
|
|
|
|
|
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
path: '.sisyphus/evidence/task-28-full-capacity.png',
|
|
|
|
|
fullPage: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('✅ Full capacity shift verified');
|
|
|
|
|
} else {
|
|
|
|
|
const shiftsGrid = page.locator('[class*="grid"], table').first();
|
|
|
|
|
await expect(shiftsGrid).toBeVisible();
|
|
|
|
|
console.log('✅ Shifts visible (full capacity scenario not found)');
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('should not allow sign-up for past shifts', async ({ page }) => {
|
2026-03-06 16:03:03 +01:00
|
|
|
await loginAs(page, 'manager@test.com', 'testpass123');
|
|
|
|
|
|
|
|
|
|
await page.goto('/shifts');
|
|
|
|
|
await page.waitForURL(/\/(shifts|select-club)/, { timeout: 10000 });
|
|
|
|
|
await selectClubIfPresent(page);
|
|
|
|
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
|
|
const pastBadges = page.locator('text=Past, span:has-text("Past")');
|
|
|
|
|
const hasPastShift = (await pastBadges.count()) > 0;
|
|
|
|
|
|
|
|
|
|
if (hasPastShift) {
|
|
|
|
|
const pastShiftCard = pastBadges.first().locator('..').locator('..');
|
|
|
|
|
const signUpInPastShift = pastShiftCard.locator('button:has-text("Sign Up")');
|
|
|
|
|
const hasSignUp = (await signUpInPastShift.count()) > 0;
|
|
|
|
|
|
|
|
|
|
expect(hasSignUp).toBe(false);
|
|
|
|
|
|
|
|
|
|
console.log('✅ Past shift has no sign-up button');
|
|
|
|
|
} else {
|
|
|
|
|
const shiftsGrid = page.locator('[class*="grid"], table').first();
|
|
|
|
|
await expect(shiftsGrid).toBeVisible();
|
|
|
|
|
console.log('✅ Shifts visible (no past shifts found)');
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
});
|
|
|
|
|
|
2026-03-06 16:03:03 +01:00
|
|
|
test('should display capacity progress bar on shifts', async ({ page }) => {
|
|
|
|
|
await loginAs(page, 'manager@test.com', 'testpass123');
|
|
|
|
|
|
|
|
|
|
await page.goto('/shifts');
|
|
|
|
|
await page.waitForURL(/\/(shifts|select-club)/, { timeout: 10000 });
|
|
|
|
|
await selectClubIfPresent(page);
|
|
|
|
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
|
|
const shiftsGrid = page.locator('[class*="grid"], table').first();
|
|
|
|
|
await expect(shiftsGrid).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const progressBars = page.locator('[role="progressbar"]');
|
|
|
|
|
const hasProgressBar = (await progressBars.count()) > 0;
|
|
|
|
|
|
|
|
|
|
if (hasProgressBar) {
|
|
|
|
|
await expect(progressBars.first()).toBeVisible();
|
|
|
|
|
console.log('✅ Progress bar visible on shifts');
|
|
|
|
|
} else {
|
|
|
|
|
const capacityText = page.locator(':has-text(/.*\\/.*spots/)');
|
|
|
|
|
const hasCapacityText = (await capacityText.count()) > 0;
|
|
|
|
|
expect(hasCapacityText).toBe(true);
|
|
|
|
|
console.log('✅ Capacity text displayed on shifts');
|
|
|
|
|
}
|
2026-03-05 10:34:03 +01:00
|
|
|
});
|
|
|
|
|
});
|