diff --git a/reactrebuild0825/playwright.config.ts b/reactrebuild0825/playwright.config.ts new file mode 100644 index 0000000..9205ce9 --- /dev/null +++ b/reactrebuild0825/playwright.config.ts @@ -0,0 +1,113 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['line'], + ['json', { outputFile: 'test-results/results.json' }] + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Video recording */ + video: 'retain-on-failure', + + /* Global timeout for actions */ + actionTimeout: 15 * 1000, + + /* Global timeout for navigation */ + navigationTimeout: 30 * 1000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 720 } + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + viewport: { width: 1280, height: 720 } + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + viewport: { width: 1280, height: 720 } + }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { + ...devices['Pixel 5'], + }, + }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + }, + }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, + + /* Global setup and teardown */ + globalSetup: require.resolve('./tests/global-setup.ts'), + + /* Timeout settings */ + timeout: 30 * 1000, + expect: { + timeout: 5 * 1000, + }, + + /* Output directories */ + outputDir: 'test-results/', +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/README.md b/reactrebuild0825/tests/README.md new file mode 100644 index 0000000..943d115 --- /dev/null +++ b/reactrebuild0825/tests/README.md @@ -0,0 +1,290 @@ +# Black Canyon Tickets - QA Test Suite + +Comprehensive Playwright-based end-to-end testing for the React rebuild of Black Canyon Tickets platform. + +## Overview + +This test suite validates critical user flows and functionality for a premium ticketing platform serving upscale venues like dance performances, weddings, and galas. + +## Test Coverage + +### ๐Ÿ”ด Critical Test Suites +- **Authentication (`auth.spec.ts`)** - Login/logout flows, protected routes, session management +- **Navigation (`navigation.spec.ts`)** - Sidebar navigation, mobile menu, breadcrumbs, routing + +### ๐ŸŸก Standard Test Suites +- **Theme Switching (`theme.spec.ts`)** - Light/dark theme transitions and persistence +- **Responsive Design (`responsive.spec.ts`)** - Mobile, tablet, desktop layouts and touch interactions +- **UI Components (`components.spec.ts`)** - Buttons, forms, cards, modals, interactive elements + +## Demo Accounts + +The following mock accounts are available for testing: + +```typescript +// Admin User +Email: admin@example.com +Password: demo123 +Organization: Black Canyon Tickets (Enterprise) + +// Organizer User +Email: organizer@example.com +Password: demo123 +Organization: Elite Events Co. (Pro) + +// Staff User +Email: staff@example.com +Password: demo123 +Organization: Wedding Venues LLC (Free) +``` + +## Quick Start + +### Prerequisites +- Node.js 18+ installed +- Application running on `localhost:5173` (Vite dev server) + +### Installation +```bash +npm install +npx playwright install +``` + +### Running Tests + +#### Full QA Suite +```bash +# Run all tests with comprehensive reporting +npm run test:qa + +# Run only critical tests (auth + navigation) +npm run test:qa:critical + +# Run with visible browser windows +npm run test:qa:headed +``` + +#### Individual Test Suites +```bash +npm run test:auth # Authentication flows +npm run test:navigation # Navigation and routing +npm run test:theme # Theme switching +npm run test:responsive # Responsive design +npm run test:components # UI components +``` + +#### Playwright Commands +```bash +# Run all tests +npm test + +# Run with Playwright UI +npm run test:ui + +# Run with visible browser +npm run test:headed + +# Run specific test file +npx playwright test tests/auth.spec.ts +``` + +## Test Reports + +After running tests, comprehensive reports are generated: + +### HTML Report +- **Location**: `./playwright-report/index.html` +- **Content**: Interactive test results with videos and traces +- **View**: Open in browser for detailed analysis + +### QA Report +- **Location**: `./test-results/qa-report.md` +- **Content**: Executive summary with pass/fail status +- **Format**: Markdown for easy sharing + +### Screenshots +- **Location**: `./screenshots/` +- **Naming**: `{test-suite}_{test-name}_{timestamp}.png` +- **Content**: Visual evidence of all test states + +## Test Architecture + +### Page Object Model +Tests use data-testid attributes for reliable element selection: + +```typescript +// Example selectors used in tests +'[data-testid="email-input"]' +'[data-testid="login-button"]' +'[data-testid="user-menu"]' +'[data-testid="nav-dashboard"]' +``` + +### Screenshot Strategy +Every test captures screenshots at key moments: +- Before/after state changes +- Error conditions +- Success confirmations +- Visual regression validation + +### Browser Support +Tests run against multiple browsers: +- Chromium (Desktop + Mobile Chrome) +- Firefox (Desktop) +- WebKit (Desktop + Mobile Safari) + +## Test Scenarios + +### Authentication Flow +``` +1. Visit homepage โ†’ redirect to login +2. Enter valid credentials โ†’ successful login +3. Navigate protected routes โ†’ verify access +4. Logout โ†’ redirect to login +5. Enter invalid credentials โ†’ show error +6. Test remember me functionality +``` + +### Navigation Flow +``` +1. Login as different user roles +2. Test sidebar navigation +3. Verify mobile menu behavior +4. Check breadcrumb updates +5. Test keyboard navigation +6. Verify active state indicators +``` + +### Theme Switching Flow +``` +1. Start in default light theme +2. Toggle to dark theme โ†’ verify visual changes +3. Refresh page โ†’ verify persistence +4. Navigate between pages โ†’ verify consistency +5. Test system preference detection +``` + +### Responsive Design Flow +``` +1. Test mobile viewport (375px) +2. Test tablet viewport (768px) +3. Test desktop viewport (1280px) +4. Verify touch interactions +5. Check orientation changes +6. Validate text scaling +``` + +### Component Testing Flow +``` +1. Test button states (default, hover, disabled) +2. Test form validation and error states +3. Test modal open/close functionality +4. Test dropdown menu interactions +5. Test loading and skeleton states +``` + +## Accessibility Testing + +Tests include accessibility validation: +- Keyboard navigation through all interactive elements +- Tab order verification +- Focus management +- Skip-to-content functionality +- ARIA attribute validation + +## Performance Considerations + +- Tests use proper wait strategies (avoid hard sleeps) +- Network throttling for realistic conditions +- Timeout configurations for different operations +- Parallel execution where safe + +## Troubleshooting + +### Common Issues + +**Tests failing due to missing application** +```bash +# Ensure dev server is running +npm run dev +# Then run tests in separate terminal +npm run test:qa +``` + +**Browser installation issues** +```bash +# Reinstall browsers +npx playwright install --force +``` + +**Screenshot permissions** +```bash +# Ensure screenshots directory exists and is writable +mkdir -p screenshots +chmod 755 screenshots +``` + +### Debug Mode +```bash +# Run specific test with debug mode +npx playwright test tests/auth.spec.ts --debug + +# Run with trace viewer +npx playwright test --trace on +``` + +## CI/CD Integration + +For continuous integration environments: + +```bash +# CI-optimized test run +CI=true npm run test:qa:critical + +# Generate JUnit reports +npx playwright test --reporter=junit +``` + +## Contributing + +When adding new tests: + +1. Follow the existing naming convention +2. Use data-testid attributes for selectors +3. Include comprehensive screenshots +4. Add both success and error scenarios +5. Update this README with new test coverage + +### Test File Structure +```typescript +// Standard test file template +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `testsuite_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); +} + +test.describe('Test Suite Name', () => { + test.beforeEach(async ({ page }) => { + // Setup code + }); + + test('should test specific functionality', async ({ page }) => { + // Test implementation + await takeScreenshot(page, 'test-description'); + }); +}); +``` + +## Contact + +For questions about the QA test suite: +- Review test failures in `./playwright-report/index.html` +- Check screenshots in `./screenshots/` directory +- Consult QA report in `./test-results/qa-report.md` \ No newline at end of file diff --git a/reactrebuild0825/tests/auth-realistic.spec.ts b/reactrebuild0825/tests/auth-realistic.spec.ts new file mode 100644 index 0000000..aeff908 --- /dev/null +++ b/reactrebuild0825/tests/auth-realistic.spec.ts @@ -0,0 +1,265 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, + organizer: { email: 'organizer@example.com', password: 'demo123' }, + staff: { email: 'staff@example.com', password: 'demo123' }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `auth_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function clearAuthStorage(page: Page) { + await page.evaluate(() => { + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); +} + +test.describe('Authentication Flows (Realistic)', () => { + test.beforeEach(async ({ page }) => { + // Clear any existing auth state + await clearAuthStorage(page); + await page.goto('/'); + }); + + test('should redirect to login when accessing protected route', async ({ page }) => { + await page.goto('/dashboard'); + + // Should redirect to login page + await expect(page).toHaveURL('/login'); + await expect(page.locator('h1')).toContainText('Black Canyon Tickets'); + await expect(page.locator('text=Sign in to your account')).toBeVisible(); + + await takeScreenshot(page, 'protected-route-redirect'); + }); + + test('should login successfully with valid admin credentials', async ({ page }) => { + await page.goto('/login'); + await takeScreenshot(page, 'login-page-initial'); + + // Fill in admin credentials using form elements + await page.fill('input[name="email"]', DEMO_ACCOUNTS.admin.email); + await page.fill('input[name="password"]', DEMO_ACCOUNTS.admin.password); + + await takeScreenshot(page, 'login-form-filled'); + + // Submit login form + await page.click('button[type="submit"]'); + + // Wait for loading state + await expect(page.locator('text=Signing in...')).toBeVisible(); + await takeScreenshot(page, 'login-loading-state'); + + // Should redirect to dashboard + await expect(page).toHaveURL('/', { timeout: 10000 }); + + // Verify user is logged in by checking for user name in sidebar + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + + await takeScreenshot(page, 'dashboard-logged-in-admin'); + }); + + test('should login successfully with organizer credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('input[name="email"]', DEMO_ACCOUNTS.organizer.email); + await page.fill('input[name="password"]', DEMO_ACCOUNTS.organizer.password); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL('/', { timeout: 10000 }); + await expect(page.locator('text=John Organizer')).toBeVisible(); + + await takeScreenshot(page, 'dashboard-logged-in-organizer'); + }); + + test('should login successfully with staff credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('input[name="email"]', DEMO_ACCOUNTS.staff.email); + await page.fill('input[name="password"]', DEMO_ACCOUNTS.staff.password); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL('/', { timeout: 10000 }); + await expect(page.locator('text=Emma Staff')).toBeVisible(); + + await takeScreenshot(page, 'dashboard-logged-in-staff'); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('input[name="email"]', 'invalid@example.com'); + await page.fill('input[name="password"]', 'wrongpassword'); + await page.click('button[type="submit"]'); + + // Should show error message + await expect(page.locator('[role="alert"]')).toBeVisible(); + await expect(page.locator('text=Invalid email or password')).toBeVisible(); + + // Should remain on login page + await expect(page).toHaveURL('/login'); + + await takeScreenshot(page, 'login-error-state'); + }); + + test('should toggle password visibility', async ({ page }) => { + await page.goto('/login'); + + const passwordInput = page.locator('input[name="password"]'); + const toggleButton = page.locator('button[type="button"]').filter({ hasText: '' }); // Eye icon button + + // Password should be hidden initially + await expect(passwordInput).toHaveAttribute('type', 'password'); + + await page.fill('input[name="password"]', 'testpassword'); + await takeScreenshot(page, 'password-hidden'); + + // Click toggle to show password + await toggleButton.click(); + await expect(passwordInput).toHaveAttribute('type', 'text'); + await takeScreenshot(page, 'password-visible'); + + // Click toggle to hide password again + await toggleButton.click(); + await expect(passwordInput).toHaveAttribute('type', 'password'); + }); + + test('should remember login when remember me is checked', async ({ page }) => { + await page.goto('/login'); + + await page.fill('input[name="email"]', DEMO_ACCOUNTS.admin.email); + await page.fill('input[name="password"]', DEMO_ACCOUNTS.admin.password); + + // Check remember me + await page.check('input[name="rememberMe"]'); + await takeScreenshot(page, 'remember-me-checked'); + + await page.click('button[type="submit"]'); + await expect(page).toHaveURL('/', { timeout: 10000 }); + + // Verify localStorage has auth data + const authData = await page.evaluate(() => localStorage.getItem('bct_auth_user')); + const rememberMe = await page.evaluate(() => localStorage.getItem('bct_auth_remember')); + + expect(authData).toBeTruthy(); + expect(rememberMe).toBe('true'); + + // Refresh page - should stay logged in + await page.reload(); + await expect(page).toHaveURL('/'); + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + + await takeScreenshot(page, 'persistent-login-after-refresh'); + }); + + test('should use demo account buttons', async ({ page }) => { + await page.goto('/login'); + + // Click admin demo account button + await page.click('text=Sarah Admin'); + + // Form should be filled + await expect(page.locator('input[name="email"]')).toHaveValue(DEMO_ACCOUNTS.admin.email); + await expect(page.locator('input[name="password"]')).toHaveValue('demo123'); + + await takeScreenshot(page, 'demo-account-filled'); + + // Submit and verify login + await page.click('button[type="submit"]'); + await expect(page).toHaveURL('/', { timeout: 10000 }); + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + + await takeScreenshot(page, 'demo-account-login-success'); + }); + + test('should validate empty form submission', async ({ page }) => { + await page.goto('/login'); + + // Try to submit empty form + await page.click('button[type="submit"]'); + + // Should show validation errors + await expect(page.locator('text=Email is required')).toBeVisible(); + + await takeScreenshot(page, 'form-validation-errors'); + }); + + test('should validate password requirement', async ({ page }) => { + await page.goto('/login'); + + // Fill email but not password + await page.fill('input[name="email"]', DEMO_ACCOUNTS.admin.email); + await page.click('button[type="submit"]'); + + await expect(page.locator('text=Password is required')).toBeVisible(); + + await takeScreenshot(page, 'password-required-error'); + }); + + test('should handle redirect after login', async ({ page }) => { + // Try to access events page directly + await page.goto('/events'); + + // Should redirect to login + await expect(page).toHaveURL('/login'); + + // Login + await page.fill('input[name="email"]', DEMO_ACCOUNTS.admin.email); + await page.fill('input[name="password"]', DEMO_ACCOUNTS.admin.password); + await page.click('button[type="submit"]'); + + // Should redirect back to events page (or dashboard for now) + await expect(page).toHaveURL(/\/(events|dashboard|$)/, { timeout: 10000 }); + + await takeScreenshot(page, 'redirect-after-login'); + }); + + test('should show loading screen during initial auth check', async ({ page }) => { + // Set up a user in localStorage to trigger auth loading + await page.evaluate(() => { + localStorage.setItem('bct_auth_user', JSON.stringify({ + id: 'test', + email: 'admin@example.com', + name: 'Test User' + })); + localStorage.setItem('bct_auth_remember', 'true'); + }); + + await page.goto('/'); + + // Should briefly show loading state + const loading = page.locator('text=Loading...'); + if (await loading.isVisible()) { + await takeScreenshot(page, 'auth-loading-screen'); + } + + // Should eventually show dashboard or redirect to login + await page.waitForTimeout(3000); + await takeScreenshot(page, 'auth-check-complete'); + }); + + test('should handle demo account role display', async ({ page }) => { + await page.goto('/login'); + + // Verify all demo accounts are shown with correct roles + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + await expect(page.locator('text=admin').first()).toBeVisible(); + + await expect(page.locator('text=John Organizer')).toBeVisible(); + await expect(page.locator('text=organizer')).toBeVisible(); + + await expect(page.locator('text=Emma Staff')).toBeVisible(); + await expect(page.locator('text=staff')).toBeVisible(); + + await takeScreenshot(page, 'demo-accounts-display'); + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/auth.spec.ts b/reactrebuild0825/tests/auth.spec.ts new file mode 100644 index 0000000..8ceabb6 --- /dev/null +++ b/reactrebuild0825/tests/auth.spec.ts @@ -0,0 +1,258 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, + organizer: { email: 'organizer@example.com', password: 'demo123' }, + staff: { email: 'staff@example.com', password: 'demo123' }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `auth_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function clearAuthStorage(page: Page) { + await page.evaluate(() => { + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); +} + +test.describe('Authentication Flows', () => { + test.beforeEach(async ({ page }) => { + // Clear any existing auth state + await clearAuthStorage(page); + await page.goto('/'); + }); + + test('should redirect to login when accessing protected route', async ({ page }) => { + await page.goto('/dashboard'); + + // Should redirect to login page + await expect(page).toHaveURL('/login'); + await expect(page.locator('h1')).toContainText('Sign In'); + + await takeScreenshot(page, 'protected-route-redirect'); + }); + + test('should login successfully with valid admin credentials', async ({ page }) => { + await page.goto('/login'); + await takeScreenshot(page, 'login-page-initial'); + + // Fill in admin credentials + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + + await takeScreenshot(page, 'login-form-filled'); + + // Submit login form + await page.click('[data-testid="login-button"]'); + + // Wait for loading state + await expect(page.locator('[data-testid="login-button"]')).toBeDisabled(); + await takeScreenshot(page, 'login-loading-state'); + + // Should redirect to dashboard + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Verify user is logged in + await expect(page.locator('[data-testid="user-menu"]')).toBeVisible(); + await expect(page.locator('[data-testid="user-name"]')).toContainText('Sarah Admin'); + + await takeScreenshot(page, 'dashboard-logged-in-admin'); + }); + + test('should login successfully with organizer credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.organizer.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.organizer.password); + await page.click('[data-testid="login-button"]'); + + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + await expect(page.locator('[data-testid="user-name"]')).toContainText('John Organizer'); + + await takeScreenshot(page, 'dashboard-logged-in-organizer'); + }); + + test('should login successfully with staff credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.staff.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.staff.password); + await page.click('[data-testid="login-button"]'); + + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + await expect(page.locator('[data-testid="user-name"]')).toContainText('Emma Staff'); + + await takeScreenshot(page, 'dashboard-logged-in-staff'); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('[data-testid="email-input"]', 'invalid@example.com'); + await page.fill('[data-testid="password-input"]', 'wrongpassword'); + await page.click('[data-testid="login-button"]'); + + // Should show error message + await expect(page.locator('[data-testid="error-message"]')).toBeVisible(); + await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid email or password'); + + // Should remain on login page + await expect(page).toHaveURL('/login'); + + await takeScreenshot(page, 'login-error-state'); + }); + + test('should show error for short password', async ({ page }) => { + await page.goto('/login'); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', '12'); // Too short + await page.click('[data-testid="login-button"]'); + + await expect(page.locator('[data-testid="error-message"]')).toBeVisible(); + await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid email or password'); + + await takeScreenshot(page, 'login-short-password-error'); + }); + + test('should toggle password visibility', async ({ page }) => { + await page.goto('/login'); + + const passwordInput = page.locator('[data-testid="password-input"]'); + const toggleButton = page.locator('[data-testid="password-toggle"]'); + + // Password should be hidden initially + await expect(passwordInput).toHaveAttribute('type', 'password'); + + await page.fill('[data-testid="password-input"]', 'testpassword'); + await takeScreenshot(page, 'password-hidden'); + + // Click toggle to show password + await toggleButton.click(); + await expect(passwordInput).toHaveAttribute('type', 'text'); + await takeScreenshot(page, 'password-visible'); + + // Click toggle to hide password again + await toggleButton.click(); + await expect(passwordInput).toHaveAttribute('type', 'password'); + }); + + test('should remember login when remember me is checked', async ({ page }) => { + await page.goto('/login'); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + + // Check remember me + await page.check('[data-testid="remember-me"]'); + await takeScreenshot(page, 'remember-me-checked'); + + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Verify localStorage has auth data + const authData = await page.evaluate(() => localStorage.getItem('bct_auth_user')); + const rememberMe = await page.evaluate(() => localStorage.getItem('bct_auth_remember')); + + expect(authData).toBeTruthy(); + expect(rememberMe).toBe('true'); + + // Refresh page - should stay logged in + await page.reload(); + await expect(page).toHaveURL('/dashboard'); + await expect(page.locator('[data-testid="user-name"]')).toContainText('Sarah Admin'); + + await takeScreenshot(page, 'persistent-login-after-refresh'); + }); + + test('should logout successfully', async ({ page }) => { + // First login + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Open user menu and logout + await page.click('[data-testid="user-menu"]'); + await takeScreenshot(page, 'user-menu-open'); + + await page.click('[data-testid="logout-button"]'); + + // Should redirect to login + await expect(page).toHaveURL('/login', { timeout: 10000 }); + + // Verify localStorage is cleared + const authData = await page.evaluate(() => localStorage.getItem('bct_auth_user')); + const rememberMe = await page.evaluate(() => localStorage.getItem('bct_auth_remember')); + + expect(authData).toBeNull(); + expect(rememberMe).toBeNull(); + + await takeScreenshot(page, 'logout-complete'); + }); + + test('should redirect back to intended route after login', async ({ page }) => { + // Try to access events page directly + await page.goto('/events'); + + // Should redirect to login with return URL + await expect(page).toHaveURL('/login'); + + // Login + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + + // Should redirect back to events page + await expect(page).toHaveURL('/events', { timeout: 10000 }); + + await takeScreenshot(page, 'redirect-to-intended-route'); + }); + + test('should handle form validation', async ({ page }) => { + await page.goto('/login'); + + // Try to submit empty form + await page.click('[data-testid="login-button"]'); + + // Should show validation errors + await expect(page.locator('[data-testid="email-error"]')).toBeVisible(); + await expect(page.locator('[data-testid="password-error"]')).toBeVisible(); + + await takeScreenshot(page, 'form-validation-errors'); + + // Fill invalid email + await page.fill('[data-testid="email-input"]', 'invalidemail'); + await page.click('[data-testid="login-button"]'); + + await expect(page.locator('[data-testid="email-error"]')).toContainText('Invalid email'); + + await takeScreenshot(page, 'invalid-email-error'); + }); + + test('should handle network errors gracefully', async ({ page }) => { + await page.goto('/login'); + + // Simulate network failure by blocking requests + await page.route('**/api/**', route => route.abort()); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + + // Should show network error + await expect(page.locator('[data-testid="error-message"]')).toBeVisible(); + + await takeScreenshot(page, 'network-error-state'); + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/components.spec.ts b/reactrebuild0825/tests/components.spec.ts new file mode 100644 index 0000000..d72d8b5 --- /dev/null +++ b/reactrebuild0825/tests/components.spec.ts @@ -0,0 +1,365 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `components_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function loginAsAdmin(page: Page) { + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); +} + +test.describe('UI Components', () => { + test.beforeEach(async ({ page }) => { + // Clear auth storage + await page.evaluate(() => { + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); + }); + + test('should display button variants correctly', async ({ page }) => { + await page.goto('/login'); + + // Primary button (login button) + const loginButton = page.locator('[data-testid="login-button"]'); + await expect(loginButton).toBeVisible(); + await expect(loginButton).toHaveClass(/bg-blue|bg-primary/); + + await takeScreenshot(page, 'button-primary-state'); + + // Test button hover state + await loginButton.hover(); + await takeScreenshot(page, 'button-primary-hover'); + + // Test button disabled state + await page.fill('[data-testid="email-input"]', 'test@example.com'); + await page.fill('[data-testid="password-input"]', 'test123'); + await loginButton.click(); + + // Button should be disabled during loading + await expect(loginButton).toBeDisabled(); + await takeScreenshot(page, 'button-primary-disabled'); + }); + + test('should display form inputs correctly', async ({ page }) => { + await page.goto('/login'); + + // Email input + const emailInput = page.locator('[data-testid="email-input"]'); + await expect(emailInput).toBeVisible(); + await expect(emailInput).toHaveAttribute('type', 'email'); + + // Password input + const passwordInput = page.locator('[data-testid="password-input"]'); + await expect(passwordInput).toBeVisible(); + await expect(passwordInput).toHaveAttribute('type', 'password'); + + await takeScreenshot(page, 'form-inputs-empty'); + + // Test input focus states + await emailInput.focus(); + await takeScreenshot(page, 'email-input-focused'); + + await passwordInput.focus(); + await takeScreenshot(page, 'password-input-focused'); + + // Test input filled states + await emailInput.fill('user@example.com'); + await passwordInput.fill('password123'); + await takeScreenshot(page, 'form-inputs-filled'); + + // Test input validation states + await emailInput.fill('invalid-email'); + await passwordInput.click(); // Trigger validation + await takeScreenshot(page, 'form-inputs-validation-error'); + }); + + test('should display cards correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to events to see cards + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Check if event cards are displayed + const cards = page.locator('[data-testid^="event-card"]'); + const cardCount = await cards.count(); + + if (cardCount > 0) { + await expect(cards.first()).toBeVisible(); + + // Test card hover effects + await cards.first().hover(); + await takeScreenshot(page, 'card-hover-effect'); + + // Test card content + await expect(cards.first().locator('.card-title, h2, h3')).toBeVisible(); + } else { + // If no cards, take screenshot of empty state + await takeScreenshot(page, 'cards-empty-state'); + } + + await takeScreenshot(page, 'cards-overview'); + }); + + test('should display badges correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Look for badges in the dashboard or events page + await page.click('[data-testid="nav-events"]'); + + // Check for status badges + const badges = page.locator('[data-testid^="badge"], .badge, [class*="badge"]'); + const badgeCount = await badges.count(); + + if (badgeCount > 0) { + await expect(badges.first()).toBeVisible(); + await takeScreenshot(page, 'badges-display'); + } + + // Navigate to dashboard to check for other badges + await page.click('[data-testid="nav-dashboard"]'); + await takeScreenshot(page, 'dashboard-badges'); + }); + + test('should display alerts correctly', async ({ page }) => { + await page.goto('/login'); + + // Trigger error alert by submitting invalid form + await page.fill('[data-testid="email-input"]', 'invalid@example.com'); + await page.fill('[data-testid="password-input"]', 'wrong'); + await page.click('[data-testid="login-button"]'); + + // Error alert should appear + const errorAlert = page.locator('[data-testid="error-message"], .alert-error, [role="alert"]'); + await expect(errorAlert).toBeVisible(); + + await takeScreenshot(page, 'alert-error-state'); + + // Check alert styling + await expect(errorAlert).toHaveClass(/error|red|danger/); + }); + + test('should display modals correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Look for modal triggers + const modalTriggers = page.locator('[data-testid*="modal"], [data-modal], [aria-haspopup="dialog"]'); + const triggerCount = await modalTriggers.count(); + + if (triggerCount > 0) { + // Click first modal trigger + await modalTriggers.first().click(); + + // Modal should appear + const modal = page.locator('[role="dialog"], .modal, [data-testid*="modal-content"]'); + await expect(modal).toBeVisible(); + + await takeScreenshot(page, 'modal-open'); + + // Modal should have overlay + const overlay = page.locator('.modal-overlay, [data-testid="modal-overlay"]'); + if (await overlay.isVisible()) { + await takeScreenshot(page, 'modal-with-overlay'); + } + + // Close modal + const closeButton = page.locator('[data-testid="modal-close"], .modal-close, [aria-label="Close"]'); + if (await closeButton.isVisible()) { + await closeButton.click(); + await expect(modal).not.toBeVisible(); + } + } + + await takeScreenshot(page, 'modal-test-complete'); + }); + + test('should display dropdown menus correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Test user menu dropdown + await page.click('[data-testid="user-menu"]'); + + const dropdown = page.locator('[data-testid="user-dropdown"]'); + await expect(dropdown).toBeVisible(); + + await takeScreenshot(page, 'dropdown-user-menu'); + + // Test dropdown items + await expect(dropdown.locator('[data-testid="profile-link"]')).toBeVisible(); + await expect(dropdown.locator('[data-testid="settings-link"]')).toBeVisible(); + await expect(dropdown.locator('[data-testid="logout-button"]')).toBeVisible(); + + // Test dropdown hover effects + await dropdown.locator('[data-testid="profile-link"]').hover(); + await takeScreenshot(page, 'dropdown-item-hover'); + + // Close dropdown by clicking outside + await page.click('body'); + await expect(dropdown).not.toBeVisible(); + }); + + test('should display loading states correctly', async ({ page }) => { + await page.goto('/login'); + + // Fill form and submit to trigger loading state + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + + // Click login and quickly capture loading state + await page.click('[data-testid="login-button"]'); + + // Button should show loading state + const loadingButton = page.locator('[data-testid="login-button"]'); + await expect(loadingButton).toBeDisabled(); + + // Check for loading spinner or text + const loadingIndicator = page.locator('[data-testid="loading-spinner"], .spinner, .loading'); + if (await loadingIndicator.isVisible()) { + await takeScreenshot(page, 'loading-spinner'); + } + + await takeScreenshot(page, 'button-loading-state'); + + // Wait for login to complete + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + }); + + test('should display skeleton loaders correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to a page that might show skeleton loaders + await page.click('[data-testid="nav-events"]'); + + // Look for skeleton components + const skeletons = page.locator('[data-testid="skeleton"], .skeleton, [class*="skeleton"]'); + const skeletonCount = await skeletons.count(); + + if (skeletonCount > 0) { + await takeScreenshot(page, 'skeleton-loaders'); + } + + // Check for loading states in general + const loadingElements = page.locator('[data-testid*="loading"], .loading, [aria-label*="loading"]'); + const loadingCount = await loadingElements.count(); + + if (loadingCount > 0) { + await takeScreenshot(page, 'loading-elements'); + } + }); + + test('should handle component interactions', async ({ page }) => { + await loginAsAdmin(page); + + // Test checkbox interactions (if any) + const checkboxes = page.locator('input[type="checkbox"]'); + const checkboxCount = await checkboxes.count(); + + if (checkboxCount > 0) { + const firstCheckbox = checkboxes.first(); + + // Test unchecked state + await expect(firstCheckbox).not.toBeChecked(); + await takeScreenshot(page, 'checkbox-unchecked'); + + // Test checked state + await firstCheckbox.check(); + await expect(firstCheckbox).toBeChecked(); + await takeScreenshot(page, 'checkbox-checked'); + } + + // Test radio button interactions (if any) + const radioButtons = page.locator('input[type="radio"]'); + const radioCount = await radioButtons.count(); + + if (radioCount > 0) { + await radioButtons.first().check(); + await takeScreenshot(page, 'radio-button-selected'); + } + + // Test select dropdown interactions (if any) + const selects = page.locator('select, [data-testid*="select"]'); + const selectCount = await selects.count(); + + if (selectCount > 0) { + const firstSelect = selects.first(); + await firstSelect.click(); + await takeScreenshot(page, 'select-dropdown-open'); + + // Select first option + const options = firstSelect.locator('option'); + if (await options.count() > 1) { + await options.nth(1).click(); + await takeScreenshot(page, 'select-option-selected'); + } + } + }); + + test('should display tooltips correctly', async ({ page }) => { + await loginAsAdmin(page); + + // Look for elements with tooltips + const tooltipTriggers = page.locator('[title], [data-tooltip], [aria-describedby]'); + const triggerCount = await tooltipTriggers.count(); + + if (triggerCount > 0) { + // Hover over first element with tooltip + await tooltipTriggers.first().hover(); + + // Look for tooltip content + const tooltips = page.locator('[role="tooltip"], .tooltip, [data-testid="tooltip"]'); + const tooltipCount = await tooltips.count(); + + if (tooltipCount > 0) { + await expect(tooltips.first()).toBeVisible(); + await takeScreenshot(page, 'tooltip-display'); + } + } + }); + + test('should handle keyboard navigation in components', async ({ page }) => { + await page.goto('/login'); + + // Test tab navigation through form + await page.keyboard.press('Tab'); // Skip to content link + await page.keyboard.press('Tab'); // Email input + await expect(page.locator('[data-testid="email-input"]')).toBeFocused(); + + await page.keyboard.press('Tab'); // Password input + await expect(page.locator('[data-testid="password-input"]')).toBeFocused(); + + await page.keyboard.press('Tab'); // Remember me checkbox + const rememberMeCheckbox = page.locator('[data-testid="remember-me"]'); + if (await rememberMeCheckbox.isVisible()) { + await expect(rememberMeCheckbox).toBeFocused(); + await page.keyboard.press('Tab'); // Login button + } + + await expect(page.locator('[data-testid="login-button"]')).toBeFocused(); + + await takeScreenshot(page, 'keyboard-navigation-login-button'); + + // Test Enter key submission + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.keyboard.press('Enter'); + + // Should submit the form + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/global-setup.ts b/reactrebuild0825/tests/global-setup.ts new file mode 100644 index 0000000..2cda8e3 --- /dev/null +++ b/reactrebuild0825/tests/global-setup.ts @@ -0,0 +1,36 @@ +import { chromium, FullConfig } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; + +async function globalSetup(config: FullConfig) { + // Ensure screenshots directory exists + const screenshotsDir = path.join(process.cwd(), 'screenshots'); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir, { recursive: true }); + } + + // Clear previous screenshots + const files = fs.readdirSync(screenshotsDir); + for (const file of files) { + if (file.endsWith('.png')) { + fs.unlinkSync(path.join(screenshotsDir, file)); + } + } + + // Optional: Pre-warm the application by visiting it once + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + await page.goto('http://localhost:5173', { waitUntil: 'networkidle' }); + console.log('โœ… Application pre-warmed successfully'); + } catch (error) { + console.warn('โš ๏ธ Could not pre-warm application:', error.message); + } finally { + await context.close(); + await browser.close(); + } +} + +export default globalSetup; \ No newline at end of file diff --git a/reactrebuild0825/tests/navigation.spec.ts b/reactrebuild0825/tests/navigation.spec.ts new file mode 100644 index 0000000..55f75f5 --- /dev/null +++ b/reactrebuild0825/tests/navigation.spec.ts @@ -0,0 +1,300 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, + organizer: { email: 'organizer@example.com', password: 'demo123' }, + staff: { email: 'staff@example.com', password: 'demo123' }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `navigation_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function loginAsAdmin(page: Page) { + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); +} + +test.describe('Navigation and Routing', () => { + test.beforeEach(async ({ page }) => { + // Clear auth storage + await page.evaluate(() => { + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); + }); + + test('should navigate through all main sections', async ({ page }) => { + await loginAsAdmin(page); + + // Take screenshot of dashboard + await takeScreenshot(page, 'dashboard-page'); + + // Navigate to Events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + await expect(page.locator('h1')).toContainText('Events'); + await takeScreenshot(page, 'events-page'); + + // Check active navigation state + await expect(page.locator('[data-testid="nav-events"]')).toHaveClass(/active|bg-/); + + // Navigate to Dashboard + await page.click('[data-testid="nav-dashboard"]'); + await expect(page).toHaveURL('/dashboard'); + await expect(page.locator('h1')).toContainText('Dashboard'); + await takeScreenshot(page, 'dashboard-page-return'); + + // Check active navigation state + await expect(page.locator('[data-testid="nav-dashboard"]')).toHaveClass(/active|bg-/); + }); + + test('should show different navigation options based on user role', async ({ page }) => { + // Test admin navigation + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Admin should see all navigation options + await expect(page.locator('[data-testid="nav-dashboard"]')).toBeVisible(); + await expect(page.locator('[data-testid="nav-events"]')).toBeVisible(); + await takeScreenshot(page, 'admin-navigation'); + + // Logout and login as staff + await page.click('[data-testid="user-menu"]'); + await page.click('[data-testid="logout-button"]'); + await expect(page).toHaveURL('/login', { timeout: 10000 }); + + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.staff.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.staff.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Staff should see limited navigation options + await expect(page.locator('[data-testid="nav-dashboard"]')).toBeVisible(); + await expect(page.locator('[data-testid="nav-events"]')).toBeVisible(); + await takeScreenshot(page, 'staff-navigation'); + }); + + test('should handle mobile navigation menu', async ({ page }) => { + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + await loginAsAdmin(page); + + await takeScreenshot(page, 'mobile-dashboard-closed-menu'); + + // Mobile menu button should be visible + await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible(); + + // Sidebar should be hidden on mobile + await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible(); + + // Open mobile menu + await page.click('[data-testid="mobile-menu-button"]'); + + // Sidebar should now be visible + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + await takeScreenshot(page, 'mobile-menu-open'); + + // Navigate to events via mobile menu + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Menu should close after navigation + await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible(); + await takeScreenshot(page, 'mobile-events-page'); + + // Test menu overlay close + await page.click('[data-testid="mobile-menu-button"]'); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + // Click overlay to close + await page.click('[data-testid="mobile-overlay"]'); + await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible(); + await takeScreenshot(page, 'mobile-menu-overlay-close'); + }); + + test('should handle desktop navigation properly', async ({ page }) => { + // Set desktop viewport + await page.setViewportSize({ width: 1280, height: 720 }); + await loginAsAdmin(page); + + // Mobile menu button should not be visible on desktop + await expect(page.locator('[data-testid="mobile-menu-button"]')).not.toBeVisible(); + + // Sidebar should be visible on desktop + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + await takeScreenshot(page, 'desktop-navigation'); + + // Test navigation without mobile menu + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + await takeScreenshot(page, 'desktop-events-navigation'); + }); + + test('should display user menu and profile information', async ({ page }) => { + await loginAsAdmin(page); + + // User menu should show user name and avatar + await expect(page.locator('[data-testid="user-menu"]')).toBeVisible(); + await expect(page.locator('[data-testid="user-name"]')).toContainText('Sarah Admin'); + await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible(); + + // Click user menu to open dropdown + await page.click('[data-testid="user-menu"]'); + + // Dropdown should show profile options + await expect(page.locator('[data-testid="user-dropdown"]')).toBeVisible(); + await expect(page.locator('[data-testid="profile-link"]')).toBeVisible(); + await expect(page.locator('[data-testid="settings-link"]')).toBeVisible(); + await expect(page.locator('[data-testid="logout-button"]')).toBeVisible(); + + await takeScreenshot(page, 'user-menu-dropdown'); + + // Click outside to close dropdown + await page.click('body'); + await expect(page.locator('[data-testid="user-dropdown"]')).not.toBeVisible(); + }); + + test('should show breadcrumb navigation', async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Should show breadcrumb + await expect(page.locator('[data-testid="breadcrumb"]')).toBeVisible(); + await expect(page.locator('[data-testid="breadcrumb"]')).toContainText('Events'); + + await takeScreenshot(page, 'breadcrumb-events'); + + // Navigate back to dashboard + await page.click('[data-testid="nav-dashboard"]'); + await expect(page).toHaveURL('/dashboard'); + + await expect(page.locator('[data-testid="breadcrumb"]')).toContainText('Dashboard'); + await takeScreenshot(page, 'breadcrumb-dashboard'); + }); + + test('should handle keyboard navigation', async ({ page }) => { + await loginAsAdmin(page); + + // Test tab navigation through main navigation + await page.keyboard.press('Tab'); + + // Skip to content link should be focused first + await expect(page.locator('[data-testid="skip-to-content"]')).toBeFocused(); + + await page.keyboard.press('Tab'); + // Navigation items should be focusable + await expect(page.locator('[data-testid="nav-dashboard"]')).toBeFocused(); + + await takeScreenshot(page, 'keyboard-nav-dashboard-focus'); + + await page.keyboard.press('Tab'); + await expect(page.locator('[data-testid="nav-events"]')).toBeFocused(); + + await takeScreenshot(page, 'keyboard-nav-events-focus'); + + // Test Enter key navigation + await page.keyboard.press('Enter'); + await expect(page).toHaveURL('/events'); + + await takeScreenshot(page, 'keyboard-nav-events-activated'); + }); + + test('should handle skip to content functionality', async ({ page }) => { + await loginAsAdmin(page); + + // Focus skip to content link + await page.keyboard.press('Tab'); + await expect(page.locator('[data-testid="skip-to-content"]')).toBeFocused(); + + await takeScreenshot(page, 'skip-to-content-focused'); + + // Activate skip to content + await page.keyboard.press('Enter'); + + // Main content should be focused + await expect(page.locator('[data-testid="main-content"]')).toBeFocused(); + + await takeScreenshot(page, 'skip-to-content-activated'); + }); + + test('should maintain navigation state across page refreshes', async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Refresh page + await page.reload(); + + // Should stay on events page with correct navigation state + await expect(page).toHaveURL('/events'); + await expect(page.locator('[data-testid="nav-events"]')).toHaveClass(/active|bg-/); + + await takeScreenshot(page, 'navigation-state-after-refresh'); + }); + + test('should handle navigation with browser back/forward buttons', async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Use browser back button + await page.goBack(); + await expect(page).toHaveURL('/dashboard'); + await expect(page.locator('[data-testid="nav-dashboard"]')).toHaveClass(/active|bg-/); + + await takeScreenshot(page, 'browser-back-navigation'); + + // Use browser forward button + await page.goForward(); + await expect(page).toHaveURL('/events'); + await expect(page.locator('[data-testid="nav-events"]')).toHaveClass(/active|bg-/); + + await takeScreenshot(page, 'browser-forward-navigation'); + }); + + test('should show loading states during navigation', async ({ page }) => { + await loginAsAdmin(page); + + // Slow down network to see loading states + await page.route('**/*', (route) => { + setTimeout(() => route.continue(), 500); + }); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + + // Should show loading state + await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible(); + await takeScreenshot(page, 'navigation-loading-state'); + + // Wait for navigation to complete + await expect(page).toHaveURL('/events'); + await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible(); + + await takeScreenshot(page, 'navigation-complete'); + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/responsive.spec.ts b/reactrebuild0825/tests/responsive.spec.ts new file mode 100644 index 0000000..c406ed0 --- /dev/null +++ b/reactrebuild0825/tests/responsive.spec.ts @@ -0,0 +1,354 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, +}; + +const VIEWPORTS = { + mobile: { width: 375, height: 667 }, + mobileLarge: { width: 414, height: 896 }, + tablet: { width: 768, height: 1024 }, + tabletLarge: { width: 1024, height: 768 }, + desktop: { width: 1280, height: 720 }, + desktopLarge: { width: 1920, height: 1080 }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `responsive_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function loginAsAdmin(page: Page) { + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); +} + +test.describe('Responsive Design', () => { + test.beforeEach(async ({ page }) => { + // Clear auth storage + await page.evaluate(() => { + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); + }); + + test('should display correctly on mobile devices', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobile); + await loginAsAdmin(page); + + // Mobile menu button should be visible + await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible(); + + // Sidebar should be hidden initially + await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible(); + + // Main content should take full width + const mainContent = page.locator('[data-testid="main-content"]'); + await expect(mainContent).toBeVisible(); + + await takeScreenshot(page, 'mobile-dashboard'); + + // Test mobile navigation + await page.click('[data-testid="mobile-menu-button"]'); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + await takeScreenshot(page, 'mobile-menu-open'); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + await takeScreenshot(page, 'mobile-events-page'); + }); + + test('should display correctly on tablet devices', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.tablet); + await loginAsAdmin(page); + + // Should still show mobile menu on smaller tablets + await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible(); + + await takeScreenshot(page, 'tablet-dashboard'); + + // Test tablet navigation + await page.click('[data-testid="mobile-menu-button"]'); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + await takeScreenshot(page, 'tablet-menu-open'); + }); + + test('should display correctly on large tablets', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.tabletLarge); + await loginAsAdmin(page); + + // Large tablets should show desktop layout + await expect(page.locator('[data-testid="mobile-menu-button"]')).not.toBeVisible(); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + await takeScreenshot(page, 'tablet-large-dashboard'); + }); + + test('should display correctly on desktop', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.desktop); + await loginAsAdmin(page); + + // Desktop should show full sidebar + await expect(page.locator('[data-testid="mobile-menu-button"]')).not.toBeVisible(); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + // Sidebar should be properly sized + const sidebar = page.locator('[data-testid="sidebar"]'); + const sidebarBox = await sidebar.boundingBox(); + expect(sidebarBox?.width).toBeGreaterThan(200); + expect(sidebarBox?.width).toBeLessThan(300); + + await takeScreenshot(page, 'desktop-dashboard'); + + // Navigate to events + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + await takeScreenshot(page, 'desktop-events-page'); + }); + + test('should handle touch interactions on mobile', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobile); + await loginAsAdmin(page); + + // Test touch tap on mobile menu + await page.tap('[data-testid="mobile-menu-button"]'); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + + // Test touch tap on navigation + await page.tap('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + await takeScreenshot(page, 'mobile-touch-navigation'); + }); + + test('should handle swipe gestures for menu', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobile); + await loginAsAdmin(page); + + // Test swipe to open menu (if implemented) + const viewport = page.viewportSize(); + if (viewport) { + // Swipe from left edge + await page.touchscreen.tap(10, viewport.height / 2); + await page.mouse.move(10, viewport.height / 2); + await page.mouse.down(); + await page.mouse.move(200, viewport.height / 2); + await page.mouse.up(); + + await takeScreenshot(page, 'mobile-swipe-gesture'); + } + }); + + test('should adapt card layouts across breakpoints', async ({ page }) => { + await loginAsAdmin(page); + + // Test mobile card layout + await page.setViewportSize(VIEWPORTS.mobile); + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Cards should stack on mobile + const cards = page.locator('[data-testid^="event-card"]'); + const cardCount = await cards.count(); + + if (cardCount > 0) { + // Check that cards are stacked (full width) + const firstCard = cards.first(); + const cardBox = await firstCard.boundingBox(); + const viewportWidth = VIEWPORTS.mobile.width; + + if (cardBox) { + expect(cardBox.width).toBeGreaterThan(viewportWidth * 0.8); + } + } + + await takeScreenshot(page, 'mobile-card-layout'); + + // Test desktop card layout + await page.setViewportSize(VIEWPORTS.desktop); + await page.waitForTimeout(500); // Allow layout to adjust + + await takeScreenshot(page, 'desktop-card-layout'); + }); + + test('should handle text scaling across devices', async ({ page }) => { + await loginAsAdmin(page); + + // Test mobile text scaling + await page.setViewportSize(VIEWPORTS.mobile); + + const mobileHeading = page.locator('h1').first(); + const mobileFontSize = await mobileHeading.evaluate((el) => + getComputedStyle(el).fontSize + ); + + await takeScreenshot(page, 'mobile-text-scaling'); + + // Test desktop text scaling + await page.setViewportSize(VIEWPORTS.desktop); + + const desktopHeading = page.locator('h1').first(); + const desktopFontSize = await desktopHeading.evaluate((el) => + getComputedStyle(el).fontSize + ); + + await takeScreenshot(page, 'desktop-text-scaling'); + + // Desktop font should generally be larger or equal + const mobileSize = parseFloat(mobileFontSize); + const desktopSize = parseFloat(desktopFontSize); + expect(desktopSize).toBeGreaterThanOrEqual(mobileSize); + }); + + test('should handle form layouts responsively', async ({ page }) => { + await page.goto('/login'); + + // Test mobile form layout + await page.setViewportSize(VIEWPORTS.mobile); + + const form = page.locator('[data-testid="login-form"]'); + await expect(form).toBeVisible(); + + // Form should take most of the width on mobile + const formBox = await form.boundingBox(); + if (formBox) { + expect(formBox.width).toBeGreaterThan(VIEWPORTS.mobile.width * 0.8); + } + + await takeScreenshot(page, 'mobile-form-layout'); + + // Test desktop form layout + await page.setViewportSize(VIEWPORTS.desktop); + + // Form should be centered and more constrained on desktop + const desktopFormBox = await form.boundingBox(); + if (desktopFormBox) { + expect(desktopFormBox.width).toBeLessThan(VIEWPORTS.desktop.width * 0.6); + } + + await takeScreenshot(page, 'desktop-form-layout'); + }); + + test('should handle button sizes across devices', async ({ page }) => { + await page.goto('/login'); + + // Test mobile button sizes + await page.setViewportSize(VIEWPORTS.mobile); + + const button = page.locator('[data-testid="login-button"]'); + const mobileButtonBox = await button.boundingBox(); + + // Buttons should be touch-friendly on mobile (min 44px height) + if (mobileButtonBox) { + expect(mobileButtonBox.height).toBeGreaterThanOrEqual(40); + } + + await takeScreenshot(page, 'mobile-button-sizes'); + + // Test desktop button sizes + await page.setViewportSize(VIEWPORTS.desktop); + + const desktopButtonBox = await button.boundingBox(); + + await takeScreenshot(page, 'desktop-button-sizes'); + + // Compare button sizes + if (mobileButtonBox && desktopButtonBox) { + console.log('Mobile button height:', mobileButtonBox.height); + console.log('Desktop button height:', desktopButtonBox.height); + } + }); + + test('should handle navigation consistency across devices', async ({ page }) => { + await loginAsAdmin(page); + + // Test that navigation items are consistent across devices + const navItems = ['Dashboard', 'Events']; + + for (const viewport of Object.values(VIEWPORTS)) { + await page.setViewportSize(viewport); + await page.waitForTimeout(500); // Allow layout to adjust + + // Open mobile menu if needed + const mobileMenuButton = page.locator('[data-testid="mobile-menu-button"]'); + if (await mobileMenuButton.isVisible()) { + await mobileMenuButton.click(); + } + + // Check that all navigation items are present + for (const item of navItems) { + await expect(page.locator(`[data-testid="nav-${item.toLowerCase()}"]`)).toBeVisible(); + } + + await takeScreenshot(page, `navigation-${viewport.width}x${viewport.height}`); + + // Close mobile menu if opened + if (await mobileMenuButton.isVisible()) { + await page.click('body'); + } + } + }); + + test('should handle image scaling and loading', async ({ page }) => { + await loginAsAdmin(page); + + // Test mobile image handling + await page.setViewportSize(VIEWPORTS.mobile); + + // Check user avatar scaling + const avatar = page.locator('[data-testid="user-avatar"]'); + if (await avatar.isVisible()) { + const avatarBox = await avatar.boundingBox(); + + if (avatarBox) { + // Avatar should be reasonably sized for mobile + expect(avatarBox.width).toBeGreaterThan(20); + expect(avatarBox.width).toBeLessThan(80); + } + } + + await takeScreenshot(page, 'mobile-image-scaling'); + + // Test desktop image handling + await page.setViewportSize(VIEWPORTS.desktop); + + if (await avatar.isVisible()) { + const desktopAvatarBox = await avatar.boundingBox(); + await takeScreenshot(page, 'desktop-image-scaling'); + + if (desktopAvatarBox) { + console.log('Desktop avatar size:', desktopAvatarBox.width, 'x', desktopAvatarBox.height); + } + } + }); + + test('should handle orientation changes on mobile', async ({ page }) => { + await loginAsAdmin(page); + + // Test portrait orientation + await page.setViewportSize({ width: 375, height: 667 }); + await takeScreenshot(page, 'mobile-portrait'); + + // Test landscape orientation + await page.setViewportSize({ width: 667, height: 375 }); + await takeScreenshot(page, 'mobile-landscape'); + + // Navigation should still work in landscape + const mobileMenuButton = page.locator('[data-testid="mobile-menu-button"]'); + if (await mobileMenuButton.isVisible()) { + await mobileMenuButton.click(); + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); + await takeScreenshot(page, 'mobile-landscape-menu-open'); + } + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/smoke.spec.ts b/reactrebuild0825/tests/smoke.spec.ts new file mode 100644 index 0000000..e48c1f0 --- /dev/null +++ b/reactrebuild0825/tests/smoke.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Smoke Tests', () => { + test('application loads successfully', async ({ page }) => { + await page.goto('/'); + + // Should either show login page or dashboard + const hasLogin = await page.locator('text=Sign in to your account').isVisible(); + const hasDashboard = await page.locator('text=Dashboard').isVisible(); + + expect(hasLogin || hasDashboard).toBeTruthy(); + + await page.screenshot({ + path: 'screenshots/smoke_application_loads.png', + fullPage: true + }); + }); + + test('login page elements are present', async ({ page }) => { + await page.goto('/login'); + + // Check for key elements + await expect(page.locator('h1')).toContainText('Black Canyon Tickets'); + await expect(page.locator('input[name="email"]')).toBeVisible(); + await expect(page.locator('input[name="password"]')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeVisible(); + + // Check for demo accounts + await expect(page.locator('text=Demo Accounts')).toBeVisible(); + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + + await page.screenshot({ + path: 'screenshots/smoke_login_elements.png', + fullPage: true + }); + }); + + test('theme toggle works', async ({ page }) => { + await page.goto('/login'); + + // Look for theme toggle button (sun or moon icon) + const themeButton = page.locator('button[aria-label*="theme"], button[aria-label*="Theme"]').first(); + + if (await themeButton.isVisible()) { + await themeButton.click(); + await page.waitForTimeout(500); + + await page.screenshot({ + path: 'screenshots/smoke_theme_toggle.png', + fullPage: true + }); + } else { + console.log('Theme toggle not found - may need to be implemented'); + } + }); + + test('basic authentication flow works', async ({ page }) => { + await page.goto('/login'); + + // Use demo account + await page.click('text=Sarah Admin'); + + // Verify form is filled + await expect(page.locator('input[name="email"]')).toHaveValue('admin@example.com'); + await expect(page.locator('input[name="password"]')).toHaveValue('demo123'); + + // Submit form + await page.click('button[type="submit"]'); + + // Wait for navigation + await page.waitForURL(/\/(dashboard|$)/, { timeout: 10000 }); + + // Should show user info + await expect(page.locator('text=Sarah Admin')).toBeVisible(); + + await page.screenshot({ + path: 'screenshots/smoke_auth_success.png', + fullPage: true + }); + }); + + test('responsive layout works', async ({ page }) => { + // Test mobile layout + await page.setViewportSize({ width: 375, height: 667 }); + await page.goto('/login'); + + await page.screenshot({ + path: 'screenshots/smoke_mobile_layout.png', + fullPage: true + }); + + // Test desktop layout + await page.setViewportSize({ width: 1280, height: 720 }); + await page.reload(); + + await page.screenshot({ + path: 'screenshots/smoke_desktop_layout.png', + fullPage: true + }); + }); +}); \ No newline at end of file diff --git a/reactrebuild0825/tests/test-runner.ts b/reactrebuild0825/tests/test-runner.ts new file mode 100644 index 0000000..017ec3d --- /dev/null +++ b/reactrebuild0825/tests/test-runner.ts @@ -0,0 +1,312 @@ +#!/usr/bin/env node + +/** + * Comprehensive test runner for Black Canyon Tickets React Rebuild + * + * This script orchestrates the execution of all test suites and generates + * comprehensive reports with screenshots for QA validation. + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; +import fs from 'fs'; +import path from 'path'; + +const execAsync = promisify(exec); + +interface TestSuite { + name: string; + file: string; + description: string; + critical: boolean; +} + +const TEST_SUITES: TestSuite[] = [ + { + name: 'Smoke Tests', + file: 'smoke.spec.ts', + description: 'Basic functionality and application health checks', + critical: true + }, + { + name: 'Authentication (Realistic)', + file: 'auth-realistic.spec.ts', + description: 'Login flows using current component selectors', + critical: true + }, + { + name: 'Authentication (Enhanced)', + file: 'auth.spec.ts', + description: 'Full auth suite requiring data-testid attributes', + critical: false + }, + { + name: 'Navigation', + file: 'navigation.spec.ts', + description: 'Sidebar navigation, mobile menu, breadcrumbs, and routing', + critical: false + }, + { + name: 'Theme Switching', + file: 'theme.spec.ts', + description: 'Light/dark theme transitions and persistence', + critical: false + }, + { + name: 'Responsive Design', + file: 'responsive.spec.ts', + description: 'Mobile, tablet, desktop layouts and touch interactions', + critical: false + }, + { + name: 'UI Components', + file: 'components.spec.ts', + description: 'Buttons, forms, cards, modals, and interactive elements', + critical: false + } +]; + +class TestRunner { + private results: { [key: string]: any } = {}; + private startTime: Date = new Date(); + + async run(options: { critical?: boolean; suite?: string; headed?: boolean } = {}) { + console.log('๐Ÿš€ Starting Black Canyon Tickets QA Test Suite'); + console.log('=' .repeat(60)); + + await this.ensureDirectories(); + await this.clearPreviousResults(); + + const suitesToRun = this.filterSuites(options); + const playwrightOptions = this.buildPlaywrightOptions(options); + + console.log(`๐Ÿ“‹ Running ${suitesToRun.length} test suite(s):`); + suitesToRun.forEach(suite => { + console.log(` ${suite.critical ? '๐Ÿ”ด' : '๐ŸŸก'} ${suite.name}: ${suite.description}`); + }); + console.log(''); + + for (const suite of suitesToRun) { + await this.runTestSuite(suite, playwrightOptions); + } + + await this.generateReport(); + + const duration = new Date().getTime() - this.startTime.getTime(); + console.log(`\nโœ… Test suite completed in ${Math.round(duration / 1000)}s`); + console.log(`๐Ÿ“Š View detailed report: ./playwright-report/index.html`); + console.log(`๐Ÿ“ธ Screenshots saved to: ./screenshots/`); + } + + private filterSuites(options: { critical?: boolean; suite?: string }): TestSuite[] { + if (options.suite) { + const suite = TEST_SUITES.find(s => s.name.toLowerCase().includes(options.suite!.toLowerCase())); + return suite ? [suite] : []; + } + + if (options.critical) { + return TEST_SUITES.filter(s => s.critical); + } + + return TEST_SUITES; + } + + private buildPlaywrightOptions(options: { headed?: boolean }): string { + const opts = []; + + if (options.headed) { + opts.push('--headed'); + } + + opts.push('--reporter=html,line'); + + return opts.join(' '); + } + + private async ensureDirectories() { + const dirs = ['screenshots', 'test-results', 'playwright-report']; + + for (const dir of dirs) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + } + + private async clearPreviousResults() { + // Clear previous screenshots + const screenshotsDir = './screenshots'; + if (fs.existsSync(screenshotsDir)) { + const files = fs.readdirSync(screenshotsDir); + for (const file of files) { + if (file.endsWith('.png')) { + fs.unlinkSync(path.join(screenshotsDir, file)); + } + } + } + } + + private async runTestSuite(suite: TestSuite, playwrightOptions: string) { + console.log(`๐Ÿงช Running ${suite.name} tests...`); + + try { + const command = `npx playwright test tests/${suite.file} ${playwrightOptions}`; + const { stdout, stderr } = await execAsync(command); + + this.results[suite.name] = { + success: true, + output: stdout, + error: null, + suite + }; + + console.log(` โœ… ${suite.name} - PASSED`); + + } catch (error: any) { + this.results[suite.name] = { + success: false, + output: error.stdout || '', + error: error.stderr || error.message, + suite + }; + + console.log(` โŒ ${suite.name} - FAILED`); + if (suite.critical) { + console.log(` ๐Ÿšจ CRITICAL TEST FAILED - Consider stopping deployment`); + } + } + } + + private async generateReport() { + const report = { + timestamp: new Date().toISOString(), + duration: new Date().getTime() - this.startTime.getTime(), + results: this.results, + summary: this.generateSummary() + }; + + // Save JSON report + fs.writeFileSync('./test-results/qa-report.json', JSON.stringify(report, null, 2)); + + // Generate markdown report + const markdown = this.generateMarkdownReport(report); + fs.writeFileSync('./test-results/qa-report.md', markdown); + + console.log('\n๐Ÿ“Š Test Summary:'); + console.log(` Total Suites: ${Object.keys(this.results).length}`); + console.log(` Passed: ${report.summary.passed}`); + console.log(` Failed: ${report.summary.failed}`); + console.log(` Critical Failed: ${report.summary.criticalFailed}`); + } + + private generateSummary() { + const total = Object.keys(this.results).length; + const passed = Object.values(this.results).filter(r => r.success).length; + const failed = total - passed; + const criticalFailed = Object.values(this.results).filter(r => !r.success && r.suite.critical).length; + + return { total, passed, failed, criticalFailed }; + } + + private generateMarkdownReport(report: any): string { + const { summary } = report; + + let markdown = `# Black Canyon Tickets QA Report\n\n`; + markdown += `**Generated:** ${new Date(report.timestamp).toLocaleString()}\n`; + markdown += `**Duration:** ${Math.round(report.duration / 1000)}s\n\n`; + + markdown += `## Summary\n\n`; + markdown += `- Total Test Suites: ${summary.total}\n`; + markdown += `- โœ… Passed: ${summary.passed}\n`; + markdown += `- โŒ Failed: ${summary.failed}\n`; + markdown += `- ๐Ÿšจ Critical Failed: ${summary.criticalFailed}\n\n`; + + if (summary.criticalFailed > 0) { + markdown += `> โš ๏ธ **WARNING:** Critical tests have failed. Consider reviewing before deployment.\n\n`; + } + + markdown += `## Test Results\n\n`; + + for (const [name, result] of Object.entries(report.results)) { + const r = result as any; + const status = r.success ? 'โœ… PASSED' : 'โŒ FAILED'; + const critical = r.suite.critical ? ' (CRITICAL)' : ''; + + markdown += `### ${name}${critical}\n\n`; + markdown += `**Status:** ${status}\n`; + markdown += `**Description:** ${r.suite.description}\n\n`; + + if (!r.success && r.error) { + markdown += `**Error:**\n\`\`\`\n${r.error}\n\`\`\`\n\n`; + } + } + + markdown += `## Screenshots\n\n`; + markdown += `All test screenshots are saved in the \`./screenshots/\` directory.\n`; + markdown += `Screenshots are organized by test suite and include timestamps.\n\n`; + + markdown += `## Next Steps\n\n`; + if (summary.criticalFailed > 0) { + markdown += `1. Review failed critical tests immediately\n`; + markdown += `2. Fix critical issues before deployment\n`; + markdown += `3. Re-run critical tests to verify fixes\n`; + } else if (summary.failed > 0) { + markdown += `1. Review failed non-critical tests\n`; + markdown += `2. Consider fixing for next iteration\n`; + markdown += `3. Document known issues if acceptable\n`; + } else { + markdown += `1. All tests passed! ๐ŸŽ‰\n`; + markdown += `2. Review screenshots for visual validation\n`; + markdown += `3. Application is ready for deployment\n`; + } + + return markdown; + } +} + +// CLI interface +if (require.main === module) { + const args = process.argv.slice(2); + const options: any = {}; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--critical': + options.critical = true; + break; + case '--suite': + options.suite = args[++i]; + break; + case '--headed': + options.headed = true; + break; + case '--help': + console.log(` +Black Canyon Tickets QA Test Runner + +Usage: npm run test:qa [options] + +Options: + --critical Run only critical test suites + --suite NAME Run specific test suite (auth, navigation, theme, responsive, components) + --headed Run tests with visible browser windows + --help Show this help message + +Examples: + npm run test:qa # Run all tests + npm run test:qa -- --critical # Run only critical tests + npm run test:qa -- --suite auth # Run only authentication tests + npm run test:qa -- --headed # Run with visible browser + `); + process.exit(0); + } + } + + const runner = new TestRunner(); + runner.run(options).catch(error => { + console.error('โŒ Test runner failed:', error); + process.exit(1); + }); +} + +export default TestRunner; \ No newline at end of file diff --git a/reactrebuild0825/tests/theme.spec.ts b/reactrebuild0825/tests/theme.spec.ts new file mode 100644 index 0000000..94b2587 --- /dev/null +++ b/reactrebuild0825/tests/theme.spec.ts @@ -0,0 +1,316 @@ +import { test, expect, Page } from '@playwright/test'; +import path from 'path'; + +const DEMO_ACCOUNTS = { + admin: { email: 'admin@example.com', password: 'demo123' }, +}; + +async function takeScreenshot(page: Page, name: string) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `theme_${name}_${timestamp}.png`; + await page.screenshot({ + path: path.join('screenshots', fileName), + fullPage: true + }); + return fileName; +} + +async function loginAsAdmin(page: Page) { + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); +} + +async function getThemeFromLocalStorage(page: Page) { + return await page.evaluate(() => localStorage.getItem('bct_theme')); +} + +async function getDocumentTheme(page: Page) { + return await page.evaluate(() => document.documentElement.classList.contains('dark')); +} + +test.describe('Theme Switching', () => { + test.beforeEach(async ({ page }) => { + // Clear theme storage + await page.evaluate(() => { + localStorage.removeItem('bct_theme'); + localStorage.removeItem('bct_auth_user'); + localStorage.removeItem('bct_auth_remember'); + }); + }); + + test('should start with default light theme', async ({ page }) => { + await page.goto('/'); + + // Should start with light theme + const isDark = await getDocumentTheme(page); + expect(isDark).toBe(false); + + await takeScreenshot(page, 'default-light-theme'); + + // Check theme toggle button shows correct state + await expect(page.locator('[data-testid="theme-toggle"]')).toBeVisible(); + }); + + test('should switch from light to dark theme', async ({ page }) => { + await loginAsAdmin(page); + + // Verify starting in light theme + let isDark = await getDocumentTheme(page); + expect(isDark).toBe(false); + await takeScreenshot(page, 'before-dark-switch'); + + // Click theme toggle + await page.click('[data-testid="theme-toggle"]'); + + // Should switch to dark theme + isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + // Verify theme is saved to localStorage + const savedTheme = await getThemeFromLocalStorage(page); + expect(savedTheme).toBe('dark'); + + await takeScreenshot(page, 'after-dark-switch'); + + // Check visual elements have dark theme classes + await expect(page.locator('body')).toHaveClass(/dark/); + }); + + test('should switch from dark to light theme', async ({ page }) => { + // Set dark theme initially + await page.evaluate(() => { + localStorage.setItem('bct_theme', 'dark'); + document.documentElement.classList.add('dark'); + }); + + await loginAsAdmin(page); + + // Verify starting in dark theme + let isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + await takeScreenshot(page, 'before-light-switch'); + + // Click theme toggle + await page.click('[data-testid="theme-toggle"]'); + + // Should switch to light theme + isDark = await getDocumentTheme(page); + expect(isDark).toBe(false); + + // Verify theme is saved to localStorage + const savedTheme = await getThemeFromLocalStorage(page); + expect(savedTheme).toBe('light'); + + await takeScreenshot(page, 'after-light-switch'); + }); + + test('should persist theme across page refreshes', async ({ page }) => { + await loginAsAdmin(page); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + let isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + // Refresh the page + await page.reload(); + + // Theme should persist + isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + const savedTheme = await getThemeFromLocalStorage(page); + expect(savedTheme).toBe('dark'); + + await takeScreenshot(page, 'dark-theme-after-refresh'); + }); + + test('should persist theme across navigation', async ({ page }) => { + await loginAsAdmin(page); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + await takeScreenshot(page, 'dark-theme-dashboard'); + + // Navigate to events page + await page.click('[data-testid="nav-events"]'); + await expect(page).toHaveURL('/events'); + + // Theme should persist + const isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + await takeScreenshot(page, 'dark-theme-events-page'); + + // Navigate back to dashboard + await page.click('[data-testid="nav-dashboard"]'); + await expect(page).toHaveURL('/dashboard'); + + // Theme should still persist + const stillDark = await getDocumentTheme(page); + expect(stillDark).toBe(true); + + await takeScreenshot(page, 'dark-theme-dashboard-return'); + }); + + test('should apply theme to all components', async ({ page }) => { + await loginAsAdmin(page); + + // Take screenshot in light theme + await takeScreenshot(page, 'components-light-theme'); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + + // Check that key components have theme applied + await expect(page.locator('[data-testid="sidebar"]')).toHaveClass(/dark/); + await expect(page.locator('[data-testid="header"]')).toHaveClass(/dark/); + await expect(page.locator('[data-testid="main-content"]')).toHaveClass(/dark/); + + await takeScreenshot(page, 'components-dark-theme'); + }); + + test('should handle system theme preference', async ({ page }) => { + // Mock system preference for dark theme + await page.emulateMedia({ colorScheme: 'dark' }); + + await page.goto('/'); + + // Should respect system preference + await takeScreenshot(page, 'system-dark-preference'); + + // Mock system preference for light theme + await page.emulateMedia({ colorScheme: 'light' }); + await page.reload(); + + await takeScreenshot(page, 'system-light-preference'); + }); + + test('should show theme toggle with correct icon', async ({ page }) => { + await loginAsAdmin(page); + + // In light theme, should show moon icon (to switch to dark) + let isDark = await getDocumentTheme(page); + expect(isDark).toBe(false); + + await expect(page.locator('[data-testid="theme-toggle"] [data-testid="moon-icon"]')).toBeVisible(); + await takeScreenshot(page, 'theme-toggle-light-mode'); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + + // In dark theme, should show sun icon (to switch to light) + isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + await expect(page.locator('[data-testid="theme-toggle"] [data-testid="sun-icon"]')).toBeVisible(); + await takeScreenshot(page, 'theme-toggle-dark-mode'); + }); + + test('should handle theme toggle keyboard interaction', async ({ page }) => { + await loginAsAdmin(page); + + // Focus the theme toggle button + await page.focus('[data-testid="theme-toggle"]'); + + // Press Enter to toggle theme + await page.keyboard.press('Enter'); + + // Should switch to dark theme + const isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + await takeScreenshot(page, 'keyboard-theme-toggle'); + + // Press Space to toggle back + await page.keyboard.press('Space'); + + // Should switch back to light theme + const isLight = await getDocumentTheme(page); + expect(isLight).toBe(false); + }); + + test('should maintain proper contrast ratios in both themes', async ({ page }) => { + await loginAsAdmin(page); + + // Test light theme contrast + await takeScreenshot(page, 'contrast-light-theme'); + + // Check text is readable against background + const lightTextColor = await page.locator('h1').evaluate((el) => + getComputedStyle(el).color + ); + const lightBgColor = await page.locator('body').evaluate((el) => + getComputedStyle(el).backgroundColor + ); + + console.log('Light theme - Text:', lightTextColor, 'Background:', lightBgColor); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + await takeScreenshot(page, 'contrast-dark-theme'); + + // Check text is readable against background + const darkTextColor = await page.locator('h1').evaluate((el) => + getComputedStyle(el).color + ); + const darkBgColor = await page.locator('body').evaluate((el) => + getComputedStyle(el).backgroundColor + ); + + console.log('Dark theme - Text:', darkTextColor, 'Background:', darkBgColor); + }); + + test('should handle rapid theme switching', async ({ page }) => { + await loginAsAdmin(page); + + // Rapidly toggle theme multiple times + for (let i = 0; i < 5; i++) { + await page.click('[data-testid="theme-toggle"]'); + await page.waitForTimeout(100); // Small delay to see the transition + } + + // Should end up in dark theme (odd number of clicks) + const isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + await takeScreenshot(page, 'rapid-theme-switching-end'); + + // Theme should be saved correctly + const savedTheme = await getThemeFromLocalStorage(page); + expect(savedTheme).toBe('dark'); + }); + + test('should handle theme on login page', async ({ page }) => { + await page.goto('/login'); + + // Theme toggle should be available on login page + await expect(page.locator('[data-testid="theme-toggle"]')).toBeVisible(); + + await takeScreenshot(page, 'login-page-light-theme'); + + // Switch to dark theme + await page.click('[data-testid="theme-toggle"]'); + + const isDark = await getDocumentTheme(page); + expect(isDark).toBe(true); + + await takeScreenshot(page, 'login-page-dark-theme'); + + // Login should maintain theme + await page.fill('[data-testid="email-input"]', DEMO_ACCOUNTS.admin.email); + await page.fill('[data-testid="password-input"]', DEMO_ACCOUNTS.admin.password); + await page.click('[data-testid="login-button"]'); + + await expect(page).toHaveURL('/dashboard', { timeout: 10000 }); + + // Theme should persist after login + const stillDark = await getDocumentTheme(page); + expect(stillDark).toBe(true); + + await takeScreenshot(page, 'dashboard-after-login-dark-theme'); + }); +}); \ No newline at end of file