441 lines
17 KiB
TypeScript
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);
|
|
});
|
|
});
|