Files
h2h-prototype/frontend/tests/rewards-comprehensive.spec.ts
2026-01-09 10:15:46 -06:00

441 lines
17 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
// Test data
const TEST_USER = {
email: 'alice@example.com',
password: 'password123',
};
// Helper to capture console errors
function setupErrorCapture(page: Page): string[] {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', err => {
errors.push(err.message);
});
return errors;
}
// Helper to filter out expected API errors (backend may not be running)
function filterCriticalErrors(errors: string[]): string[] {
return errors.filter(e =>
!e.includes('Failed to load resource') &&
!e.includes('ERR_CONNECTION_REFUSED') &&
!e.includes('NetworkError') &&
!e.includes('net::ERR')
);
}
test.describe('Home Page Loading', () => {
test('should load home page without getting stuck in spinner', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/', { waitUntil: 'domcontentloaded' });
// Wait for initial content
await page.waitForTimeout(3000);
// Check H2H branding is visible
await expect(page.locator('header h1:has-text("H2H")')).toBeVisible({ timeout: 5000 });
// Page should show content, not just a spinner
// Either shows loading state briefly or actual content
const hasContent = await page.locator('main').isVisible() ||
await page.locator('[class*="container"]').isVisible();
expect(hasContent).toBe(true);
// Check for critical errors (not API errors from missing backend)
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('should show H2H header branding', async ({ page }) => {
await page.goto('/');
await expect(page.locator('header h1:has-text("H2H")')).toBeVisible({ timeout: 5000 });
});
test('should have navigation links including Rewards', async ({ page }) => {
await page.goto('/');
// Check Rewards navigation exists (dropdown button)
await expect(page.locator('nav button:has-text("Rewards"), nav a:has-text("Rewards")').first()).toBeVisible({ timeout: 5000 });
});
});
test.describe('Rewards Pages Navigation', () => {
test('should navigate to /rewards overview page', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// Check page title
await expect(page.locator('h1:has-text("Rewards")')).toBeVisible({ timeout: 5000 });
// Check for no critical errors
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('should navigate to /rewards/leaderboard page', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards/leaderboard', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// Check page title
await expect(page.locator('h1:has-text("Leaderboard")')).toBeVisible({ timeout: 5000 });
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('should navigate to /rewards/achievements page', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards/achievements', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// Check page title
await expect(page.locator('h1:has-text("Achievements")')).toBeVisible({ timeout: 5000 });
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('should navigate to /rewards/loot-boxes page', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards/loot-boxes', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// Check page title
await expect(page.locator('h1:has-text("Loot Boxes")')).toBeVisible({ timeout: 5000 });
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('should navigate to /rewards/activity page', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards/activity', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// Check page title
await expect(page.locator('h1:has-text("Activity")')).toBeVisible({ timeout: 5000 });
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
});
test.describe('Header Rewards Dropdown', () => {
test('should show Rewards dropdown button in header', async ({ page }) => {
await page.goto('/');
// Find the Rewards dropdown button
const rewardsButton = page.locator('nav button:has-text("Rewards")').first();
await expect(rewardsButton).toBeVisible({ timeout: 5000 });
});
test('should open dropdown menu when clicking Rewards', async ({ page }) => {
await page.goto('/');
// Click Rewards dropdown
const rewardsButton = page.locator('nav button:has-text("Rewards")').first();
await rewardsButton.click();
// Check dropdown menu appears with all options
await expect(page.locator('a[href="/rewards"]')).toBeVisible({ timeout: 3000 });
await expect(page.locator('a[href="/rewards/leaderboard"]')).toBeVisible({ timeout: 3000 });
await expect(page.locator('a[href="/rewards/achievements"]')).toBeVisible({ timeout: 3000 });
await expect(page.locator('a[href="/rewards/loot-boxes"]')).toBeVisible({ timeout: 3000 });
await expect(page.locator('a[href="/rewards/activity"]')).toBeVisible({ timeout: 3000 });
});
test('should navigate to rewards overview from dropdown', async ({ page }) => {
await page.goto('/');
// Click Rewards dropdown
await page.locator('nav button:has-text("Rewards")').first().click();
// Click Overview link
await page.locator('a[href="/rewards"]:has-text("Overview")').click();
// Verify navigation
await expect(page).toHaveURL('/rewards');
await expect(page.locator('h1:has-text("Rewards")')).toBeVisible({ timeout: 5000 });
});
test('should navigate to leaderboard from dropdown', async ({ page }) => {
await page.goto('/');
await page.locator('nav button:has-text("Rewards")').first().click();
await page.locator('a[href="/rewards/leaderboard"]').click();
await expect(page).toHaveURL('/rewards/leaderboard');
await expect(page.locator('h1:has-text("Leaderboard")')).toBeVisible({ timeout: 5000 });
});
test('should close dropdown when clicking outside', async ({ page }) => {
await page.goto('/');
// Open dropdown
await page.locator('nav button:has-text("Rewards")').first().click();
await expect(page.locator('a[href="/rewards/leaderboard"]')).toBeVisible();
// Click outside
await page.locator('header h1:has-text("H2H")').click();
// Dropdown should close (link should not be visible in dropdown context)
await page.waitForTimeout(500);
// The dropdown should be hidden
const dropdown = page.locator('div.absolute a[href="/rewards/leaderboard"]');
await expect(dropdown).not.toBeVisible({ timeout: 2000 });
});
});
test.describe('Theme Consistency - Light Theme', () => {
test('rewards overview page should have light theme with white backgrounds', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Check for Header component
await expect(page.locator('header')).toBeVisible();
// Check header has white/light background
const header = page.locator('header');
await expect(header).toHaveCSS('background-color', 'rgb(255, 255, 255)');
// Check page background is light (gray-50 = rgb(249, 250, 251))
const body = page.locator('div.min-h-screen');
const bgColor = await body.evaluate((el) => window.getComputedStyle(el).backgroundColor);
expect(['rgb(249, 250, 251)', 'rgb(248, 250, 252)']).toContain(bgColor);
});
test('leaderboard page should have light theme', async ({ page }) => {
await page.goto('/rewards/leaderboard');
await page.waitForTimeout(2000);
// Check for Header component
await expect(page.locator('header')).toBeVisible();
// Page title should be dark text
const title = page.locator('h1:has-text("Leaderboard")');
await expect(title).toHaveCSS('color', 'rgb(17, 24, 39)'); // gray-900
});
test('achievements page should have light theme', async ({ page }) => {
await page.goto('/rewards/achievements');
await page.waitForTimeout(2000);
await expect(page.locator('header')).toBeVisible();
const title = page.locator('h1:has-text("Achievements")');
await expect(title).toHaveCSS('color', 'rgb(17, 24, 39)');
});
test('loot boxes page should have light theme', async ({ page }) => {
await page.goto('/rewards/loot-boxes');
await page.waitForTimeout(2000);
await expect(page.locator('header')).toBeVisible();
const title = page.locator('h1:has-text("Loot Boxes")');
await expect(title).toHaveCSS('color', 'rgb(17, 24, 39)');
});
test('activity page should have light theme', async ({ page }) => {
await page.goto('/rewards/activity');
await page.waitForTimeout(2000);
await expect(page.locator('header')).toBeVisible();
const title = page.locator('h1:has-text("Activity")');
await expect(title).toHaveCSS('color', 'rgb(17, 24, 39)');
});
});
test.describe('Authentication Gates', () => {
test('achievements page should show "Sign In Required" when not logged in', async ({ page }) => {
await page.goto('/rewards/achievements');
await page.waitForTimeout(2000);
// Should show sign in required message
await expect(page.locator('text=Sign In Required')).toBeVisible({ timeout: 5000 });
// Should have sign in button
await expect(page.locator('a[href="/login"]:has-text("Sign In")')).toBeVisible();
});
test('loot boxes page should show "Sign In Required" when not logged in', async ({ page }) => {
await page.goto('/rewards/loot-boxes');
await page.waitForTimeout(2000);
// Should show sign in required message
await expect(page.locator('text=Sign In Required')).toBeVisible({ timeout: 5000 });
// Should have sign in button
await expect(page.locator('a[href="/login"]:has-text("Sign In")')).toBeVisible();
});
test('rewards overview should show "Sign In to Track Progress" when not logged in', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Should show sign in prompt
await expect(page.locator('text=Sign In to Track Progress')).toBeVisible({ timeout: 5000 });
});
test('leaderboard page should be accessible without auth', async ({ page }) => {
await page.goto('/rewards/leaderboard');
await page.waitForTimeout(2000);
// Should NOT show sign in required
await expect(page.locator('text=Sign In Required')).not.toBeVisible();
// Should show leaderboard content area
await expect(page.locator('h1:has-text("Leaderboard")')).toBeVisible();
});
test('activity page should be accessible without auth', async ({ page }) => {
await page.goto('/rewards/activity');
await page.waitForTimeout(2000);
// Should NOT show sign in required
await expect(page.locator('text=Sign In Required')).not.toBeVisible();
// Should show activity page
await expect(page.locator('h1:has-text("Activity")')).toBeVisible();
});
});
test.describe('Gamification Component Rendering', () => {
test('Leaderboard component should render on rewards page', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Should have Leaderboard section with trophy icon/heading
await expect(page.locator('h3:has-text("Leaderboard")').first()).toBeVisible({ timeout: 5000 });
});
test('Leaderboard component should have category tabs', async ({ page }) => {
await page.goto('/rewards/leaderboard');
await page.waitForTimeout(2000);
// Check for category buttons
await expect(page.locator('button:has-text("Top Earners")').first()).toBeVisible({ timeout: 5000 });
await expect(page.locator('button:has-text("High Rollers")').first()).toBeVisible();
await expect(page.locator('button:has-text("Most Wins")').first()).toBeVisible();
});
test('WhaleTracker component should render on rewards page', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// WhaleTracker should be visible (may have whale icon or "Whale" text)
const whaleSection = page.locator('text=/whale/i').first();
const isVisible = await whaleSection.isVisible().catch(() => false);
// May show loading or content - just verify the section exists
expect(isVisible || await page.locator('.bg-white.rounded-xl').count() > 0).toBe(true);
});
test('rewards overview should have quick links section', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Quick links to sub-pages
await expect(page.locator('a[href="/rewards/leaderboard"]').first()).toBeVisible({ timeout: 5000 });
await expect(page.locator('a[href="/rewards/achievements"]').first()).toBeVisible();
await expect(page.locator('a[href="/rewards/loot-boxes"]').first()).toBeVisible();
await expect(page.locator('a[href="/rewards/activity"]').first()).toBeVisible();
});
test('rewards overview should have "How Tiers Work" section', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
await expect(page.locator('text=How Tiers Work')).toBeVisible({ timeout: 5000 });
});
test('activity page should render ActivityFeed component', async ({ page }) => {
await page.goto('/rewards/activity');
await page.waitForTimeout(2000);
// Activity page should show feed area (may be empty or loading)
// The page should render without errors
await expect(page.locator('h1:has-text("Activity")')).toBeVisible();
});
});
test.describe('Sub-Navigation on Rewards Pages', () => {
test('rewards pages should have sub-navigation tabs', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Check for sub-nav with all links
const subNav = page.locator('nav a[href="/rewards"]');
await expect(subNav.first()).toBeVisible({ timeout: 5000 });
await expect(page.locator('nav a[href="/rewards/leaderboard"]').first()).toBeVisible();
await expect(page.locator('nav a[href="/rewards/achievements"]').first()).toBeVisible();
await expect(page.locator('nav a[href="/rewards/loot-boxes"]').first()).toBeVisible();
await expect(page.locator('nav a[href="/rewards/activity"]').first()).toBeVisible();
});
test('sub-navigation should highlight active page', async ({ page }) => {
await page.goto('/rewards/leaderboard');
await page.waitForTimeout(2000);
// The Leaderboard tab should have the active styling (border-primary)
const leaderboardTab = page.locator('nav a[href="/rewards/leaderboard"]').first();
await expect(leaderboardTab).toHaveClass(/border-primary|text-primary/);
});
test('clicking sub-nav should navigate between pages', async ({ page }) => {
await page.goto('/rewards');
await page.waitForTimeout(2000);
// Click Achievements in sub-nav
await page.locator('nav a[href="/rewards/achievements"]').first().click();
await expect(page).toHaveURL('/rewards/achievements');
await expect(page.locator('h1:has-text("Achievements")')).toBeVisible({ timeout: 5000 });
// Click Activity
await page.locator('nav a[href="/rewards/activity"]').first().click();
await expect(page).toHaveURL('/rewards/activity');
await expect(page.locator('h1:has-text("Activity")')).toBeVisible({ timeout: 5000 });
});
});
test.describe('Error Handling', () => {
test('rewards page should handle API errors gracefully', async ({ page }) => {
const errors = setupErrorCapture(page);
await page.goto('/rewards');
await page.waitForTimeout(3000);
// Page should still render even if API fails
await expect(page.locator('h1:has-text("Rewards")')).toBeVisible({ timeout: 5000 });
// Should not have JS errors (API errors are expected)
const criticalErrors = filterCriticalErrors(errors);
expect(criticalErrors).toHaveLength(0);
});
test('leaderboard should show empty state or loading when API unavailable', async ({ page }) => {
await page.goto('/rewards/leaderboard');
await page.waitForTimeout(3000);
// Should either show loading skeleton, empty state, or actual data
const hasContent =
await page.locator('.animate-pulse').count() > 0 ||
await page.locator('text=No entries').isVisible() ||
await page.locator('text=/\\#[0-9]+|🥇|🥈|🥉/').count() > 0;
expect(hasContent).toBe(true);
});
});