feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
310
reactrebuild0825/tests/scan-offline.spec.ts
Normal file
310
reactrebuild0825/tests/scan-offline.spec.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Comprehensive Scanner Tests
|
||||
* Tests offline functionality, background sync, and conflict resolution
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Scanner Offline Functionality', () => {
|
||||
const testEventId = 'evt-1';
|
||||
const testQRCode = 'TICKET_123456';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
// Grant camera permissions for testing
|
||||
await context.grantPermissions(['camera']);
|
||||
|
||||
// Login as staff user who has scan:tickets permission
|
||||
await page.goto('/login');
|
||||
await page.fill('[name="email"]', 'staff@example.com');
|
||||
await page.fill('[name="password"]', 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard');
|
||||
});
|
||||
|
||||
test('should display scanner page with event ID', async ({ page }) => {
|
||||
// Navigate to scanner with event ID
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Should show scanner interface
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
await expect(page.locator('text=Device · No Zone Set')).toBeVisible();
|
||||
|
||||
// Should show camera view
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
|
||||
// Should show scanner frame overlay
|
||||
await expect(page.locator('.border-primary-500')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should reject access without event ID', async ({ page }) => {
|
||||
await page.goto('/scan');
|
||||
|
||||
// Should show error message
|
||||
await expect(page.locator('text=Event ID Required')).toBeVisible();
|
||||
await expect(page.locator('text=Please access the scanner with a valid event ID parameter')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show online status and stats', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Should show online badge
|
||||
await expect(page.locator('text=Online')).toBeVisible();
|
||||
|
||||
// Should show stats
|
||||
await expect(page.locator('text=Total:')).toBeVisible();
|
||||
await expect(page.locator('text=Pending:')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open and close settings panel', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Click settings button
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
|
||||
// Should show settings panel
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
await expect(page.locator('text=Optimistic Accept')).toBeVisible();
|
||||
|
||||
// Test zone setting
|
||||
await page.fill('input[placeholder*="Gate"]', 'Gate A');
|
||||
await expect(page.locator('input[placeholder*="Gate"]')).toHaveValue('Gate A');
|
||||
|
||||
// Close settings panel
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await expect(page.locator('text=Gate/Zone')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should toggle optimistic accept setting', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Open settings
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
|
||||
// Find the toggle button (it's a button with inline-flex class)
|
||||
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
|
||||
|
||||
// Get initial state by checking if bg-primary-500 class is present
|
||||
const isInitiallyOn = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
|
||||
|
||||
// Click toggle
|
||||
await toggle.click();
|
||||
|
||||
// Verify state changed
|
||||
const isAfterClickOn = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
|
||||
expect(isAfterClickOn).toBe(!isInitiallyOn);
|
||||
});
|
||||
|
||||
test('should handle torch toggle when supported', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Look for torch button (only visible if torch is supported)
|
||||
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
||||
|
||||
// If torch is supported, test the toggle
|
||||
if (await torchButton.isVisible()) {
|
||||
await torchButton.click();
|
||||
// Note: Actual torch functionality can't be tested in headless mode
|
||||
// but we can verify the button click doesn't cause errors
|
||||
}
|
||||
});
|
||||
|
||||
test('should simulate offline mode and queueing', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Simulate going offline
|
||||
await page.evaluate(() => {
|
||||
// Override navigator.onLine
|
||||
Object.defineProperty(navigator, 'onLine', {
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
|
||||
// Dispatch offline event
|
||||
window.dispatchEvent(new Event('offline'));
|
||||
});
|
||||
|
||||
// Wait for offline status to update
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Should show offline badge
|
||||
await expect(page.locator('text=Offline')).toBeVisible();
|
||||
|
||||
// Simulate a scan by calling the scan handler directly
|
||||
await page.evaluate((qr) => {
|
||||
// Trigger scan event
|
||||
window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr } }));
|
||||
}, testQRCode);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should show pending sync count increased
|
||||
// (This test is limited since we can't actually scan QR codes in automated tests)
|
||||
});
|
||||
|
||||
test('should show scan result with success status', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Mock a successful scan result by triggering the UI update directly
|
||||
await page.evaluate(() => {
|
||||
// Simulate a successful scan result
|
||||
const event = new CustomEvent('mock-scan-result', {
|
||||
detail: {
|
||||
qr: 'TICKET_123456',
|
||||
status: 'success',
|
||||
message: 'Valid ticket - Entry allowed',
|
||||
timestamp: Date.now(),
|
||||
ticketInfo: {
|
||||
eventTitle: 'Test Event',
|
||||
ticketTypeName: 'General Admission',
|
||||
customerEmail: 'test@example.com'
|
||||
}
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify we don't see error states (since this is a mock test environment)
|
||||
await expect(page.locator('text=Ticket Scanner')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle camera permission denied', async ({ page, context }) => {
|
||||
// Revoke camera permission
|
||||
await context.clearPermissions();
|
||||
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should show permission denied message or retry button
|
||||
// Note: In a real test environment, you might see different behaviors
|
||||
// depending on how the camera permission is handled
|
||||
|
||||
// Verify the page still loads without crashing
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display instructions', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Should show instructions card
|
||||
await expect(page.locator('text=Instructions')).toBeVisible();
|
||||
await expect(page.locator('text=Position QR code within the scanning frame')).toBeVisible();
|
||||
await expect(page.locator('text=Scans work offline and sync automatically')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle navigation away and back', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Verify scanner loads
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Navigate away
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible();
|
||||
|
||||
// Navigate back to scanner
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Should reinitialize properly
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be responsive on mobile viewport', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Should still show all essential elements
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
|
||||
// Settings should still be accessible
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should maintain zone setting across page reloads', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Set zone
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await page.fill('input[placeholder*="Gate"]', 'Gate A');
|
||||
|
||||
// Reload page
|
||||
await page.reload();
|
||||
|
||||
// Zone should be preserved
|
||||
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await expect(page.locator('input[placeholder*="Gate"]')).toHaveValue('Gate A');
|
||||
});
|
||||
|
||||
test('should handle service worker registration', async ({ page }) => {
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Check that service worker is being registered
|
||||
const swRegistration = await page.evaluate(() => 'serviceWorker' in navigator);
|
||||
|
||||
expect(swRegistration).toBe(true);
|
||||
|
||||
// Verify PWA manifest is linked
|
||||
const manifestLink = await page.locator('link[rel="manifest"]').getAttribute('href');
|
||||
expect(manifestLink).toBe('/manifest.json');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Scanner Access Control', () => {
|
||||
test('should require authentication', async ({ page }) => {
|
||||
// Try to access scanner without login
|
||||
await page.goto('/scan?eventId=evt-1');
|
||||
|
||||
// Should redirect to login
|
||||
await page.waitForURL('/login');
|
||||
await expect(page.locator('h1:has-text("Login")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should require scan:tickets permission', async ({ page }) => {
|
||||
// Login as a user without scan permissions (simulate by modifying role)
|
||||
await page.goto('/login');
|
||||
await page.fill('[name="email"]', 'customer@example.com'); // Non-existent user
|
||||
await page.fill('[name="password"]', 'password');
|
||||
|
||||
// This should either fail login or redirect to unauthorized
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Expect to either stay on login or go to error page
|
||||
const url = page.url();
|
||||
expect(url).toMatch(/(login|unauthorized|error)/);
|
||||
});
|
||||
|
||||
test('should allow access for staff role', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('[name="email"]', 'staff@example.com');
|
||||
await page.fill('[name="password"]', 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
// Navigate to scanner
|
||||
await page.goto('/scan?eventId=evt-1');
|
||||
|
||||
// Should have access
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow access for admin role', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('[name="email"]', 'admin@example.com');
|
||||
await page.fill('[name="password"]', 'password');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
// Navigate to scanner
|
||||
await page.goto('/scan?eventId=evt-1');
|
||||
|
||||
// Should have access
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user