Files
blackcanyontickets/reactrebuild0825/tests/offline-scenarios.spec.ts
dzinesco d5c3953888 fix(typescript): resolve build errors and improve type safety
- Fix billing components ConnectError type compatibility with exactOptionalPropertyTypes
- Update Select component usage to match proper API (options vs children)
- Remove unused imports and fix optional property assignments in system components
- Resolve duplicate Order/Ticket type definitions and add null safety checks
- Handle optional branding properties correctly in organization features
- Add window property type declarations for test environment
- Fix Playwright API usage (page.setOffline → page.context().setOffline)
- Clean up unused imports, variables, and parameters across codebase
- Add comprehensive global type declarations for test window extensions

Resolves major TypeScript compilation issues and improves type safety throughout the application.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 13:31:19 -06:00

607 lines
21 KiB
TypeScript

/**
* Offline Scanning Scenarios - Comprehensive Offline Functionality Testing
* Tests airplane mode simulation, intermittent connections, optimistic acceptance,
* conflict resolution, and queue persistence for real gate operations
*/
import { test, expect } from '@playwright/test';
test.describe('Airplane Mode Simulation', () => {
const testEventId = 'evt-001';
const testQRCodes = [
'TICKET_OFFLINE_001',
'TICKET_OFFLINE_002',
'TICKET_OFFLINE_003',
'TICKET_OFFLINE_004',
'TICKET_OFFLINE_005'
];
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
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=${testEventId}`);
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
});
test('should handle complete offline mode with queue accumulation', async ({ page }) => {
// Enable optimistic accept for offline scanning
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
// Ensure optimistic accept is enabled
const isOptimisticEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isOptimisticEnabled) {
await toggle.click();
}
// Set zone for identification
await page.fill('input[placeholder*="Gate"]', 'Gate A - Offline Test');
// Close settings
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Verify initial online state
await expect(page.locator('text=Online')).toBeVisible();
// Simulate complete network failure (airplane mode)
await page.context().setOffline(true);
// Also simulate navigator.onLine false
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: false
});
window.dispatchEvent(new Event('offline'));
});
// Wait for offline status update
await page.waitForTimeout(2000);
await expect(page.locator('text=Offline')).toBeVisible();
// Simulate scanning 5 QR codes while offline
for (let i = 0; i < testQRCodes.length; i++) {
await page.evaluate((qr) => {
// Simulate QR code scan
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, testQRCodes[i]);
await page.waitForTimeout(500);
}
// Check pending sync counter increased
const pendingText = await page.locator('text=Pending:').locator('..').textContent();
expect(pendingText).toContain('5');
// Simulate network restoration
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: true
});
window.dispatchEvent(new Event('online'));
});
// Wait for reconnection and sync
await page.waitForTimeout(3000);
await expect(page.locator('text=Online')).toBeVisible();
// Pending count should decrease as items sync
// Note: In a real implementation, this would show the sync progress
await page.waitForTimeout(2000);
});
test('should preserve queue across browser refresh while offline', async ({ page }) => {
// Go offline first
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
await expect(page.locator('text=Offline')).toBeVisible();
// Enable optimistic accept
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isEnabled) {await toggle.click();}
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Simulate scanning while offline
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, 'TICKET_PERSIST_001');
await page.waitForTimeout(1000);
// Refresh the page while offline
await page.reload();
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Should still show offline status
await expect(page.locator('text=Offline')).toBeVisible();
// Queue should be preserved (would check IndexedDB in real implementation)
// For now, verify the UI is consistent
const pendingElement = page.locator('text=Pending:').locator('..');
await expect(pendingElement).toBeVisible();
});
test('should handle optimistic acceptance UI feedback', async ({ page }) => {
// Enable optimistic accept
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isEnabled) {await toggle.click();}
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Go offline
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
await expect(page.locator('text=Offline')).toBeVisible();
// Simulate scan and check for optimistic UI feedback
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('mock-scan-result', {
detail: {
qr: 'TICKET_OPTIMISTIC_001',
status: 'offline_success',
message: 'Accepted offline - Will sync when online',
timestamp: Date.now(),
optimistic: true
}
}));
});
await page.waitForTimeout(500);
// Should show optimistic success feedback
// (Implementation would show green flash or success indicator)
await expect(page.locator('text=Offline')).toBeVisible();
});
});
test.describe('Intermittent Connectivity', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
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');
await page.goto(`/scan?eventId=${testEventId}`);
});
test('should handle flaky network with connect/disconnect cycles', async ({ page }) => {
// Start online
await expect(page.locator('text=Online')).toBeVisible();
// Simulate flaky network - multiple connect/disconnect cycles
for (let cycle = 0; cycle < 3; cycle++) {
// Go offline
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
await page.waitForTimeout(1000);
await expect(page.locator('text=Offline')).toBeVisible();
// Simulate scan during offline period
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr: `TICKET_FLAKY_${cycle}`, timestamp: Date.now() }
}));
}, `TICKET_FLAKY_${cycle}`);
await page.waitForTimeout(500);
// Come back online briefly
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
window.dispatchEvent(new Event('online'));
});
await page.waitForTimeout(1500);
// May briefly show online before next disconnect
}
// Should handle the instability gracefully
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
});
test('should retry failed requests during poor connectivity', async ({ page }) => {
// Simulate poor connectivity with high failure rate
let requestCount = 0;
await page.route('**/api/scan/**', (route) => {
requestCount++;
if (requestCount <= 3) {
// First 3 requests fail
route.abort();
} else {
// 4th request succeeds
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
valid: true,
message: 'Entry allowed',
latencyMs: 2500
})
});
}
});
// Simulate scan
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr: 'TICKET_RETRY_001', timestamp: Date.now() }
}));
});
// Should eventually succeed after retries
await page.waitForTimeout(5000);
expect(requestCount).toBeGreaterThan(1);
});
test('should show connection quality indicators', async ({ page }) => {
// Mock slow network responses
await page.route('**/*', (route) => {
setTimeout(() => route.continue(), Math.random() * 1000 + 500);
});
await page.reload();
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Should show online but potentially with latency indicators
await expect(page.locator('text=Online')).toBeVisible();
// Could show latency warnings or connection quality
// (Implementation dependent)
});
});
test.describe('Conflict Resolution', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
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');
await page.goto(`/scan?eventId=${testEventId}`);
});
test('should handle offline success vs server already_scanned conflict', async ({ page }) => {
// Enable optimistic accept
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isEnabled) {await toggle.click();}
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Go offline
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
// Scan ticket offline (optimistically accepted)
const conflictQR = 'TICKET_CONFLICT_001';
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan-result', {
detail: {
qr,
status: 'offline_success',
message: 'Accepted offline',
timestamp: Date.now(),
optimistic: true
}
}));
}, conflictQR);
await page.waitForTimeout(500);
// Mock server response indicating already scanned when syncing
await page.route('**/api/scan/**', (route) => {
route.fulfill({
status: 409,
contentType: 'application/json',
body: JSON.stringify({
valid: false,
reason: 'already_scanned',
scannedAt: '2023-10-15T10:30:00Z',
message: 'Ticket already scanned at 10:30 AM'
})
});
});
// Come back online
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
window.dispatchEvent(new Event('online'));
});
await page.waitForTimeout(3000);
// Should handle conflict gracefully
await expect(page.locator('text=Online')).toBeVisible();
// Would show conflict resolution UI in real implementation
});
test('should handle duplicate scan prevention', async ({ page }) => {
const duplicateQR = 'TICKET_DUPLICATE_001';
// First scan - should succeed
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, duplicateQR);
await page.waitForTimeout(1000);
// Second scan of same QR within rate limit window - should be prevented
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, duplicateQR);
await page.waitForTimeout(500);
// Should prevent duplicate scan (implementation would show warning)
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
});
test('should resolve conflicts with administrator review', async ({ page }) => {
// Simulate conflict scenario requiring admin intervention
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('conflict-detected', {
detail: {
qr: 'TICKET_ADMIN_CONFLICT_001',
localResult: 'offline_success',
serverResult: {
valid: false,
reason: 'already_scanned',
scannedAt: '2023-10-15T09:45:00Z'
},
requiresReview: true
}
}));
});
await page.waitForTimeout(1000);
// Should show conflict indication
// (Real implementation would show conflict modal or alert)
await expect(page.locator('text=Online')).toBeVisible();
});
});
test.describe('Queue Persistence and Sync', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
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');
await page.goto(`/scan?eventId=${testEventId}`);
});
test('should persist queue through browser restart', async ({ page, context }) => {
// Go offline and scan items
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
// Enable optimistic scanning
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isEnabled) {await toggle.click();}
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Scan multiple items
for (let i = 1; i <= 3; i++) {
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, `TICKET_PERSIST_${i}`);
await page.waitForTimeout(300);
}
// Close and reopen browser (simulate restart)
await page.close();
const newPage = await context.newPage();
await newPage.goto('/login');
await newPage.fill('[name="email"]', 'staff@example.com');
await newPage.fill('[name="password"]', 'password');
await newPage.click('button[type="submit"]');
await newPage.waitForURL('/dashboard');
await newPage.goto(`/scan?eventId=${testEventId}`);
await expect(newPage.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Queue should be restored from IndexedDB
// (Implementation would show pending items from storage)
const pendingElement = newPage.locator('text=Pending:').locator('..');
await expect(pendingElement).toBeVisible();
});
test('should handle sync failure recovery', async ({ page }) => {
// Create offline queue
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
window.dispatchEvent(new Event('offline'));
});
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr: 'TICKET_SYNC_FAIL_001', timestamp: Date.now() }
}));
});
// Mock sync failure
await page.route('**/api/scan/**', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
});
});
// Come back online (sync should fail)
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
window.dispatchEvent(new Event('online'));
});
await page.waitForTimeout(3000);
// Should show failed sync status
await expect(page.locator('text=Online')).toBeVisible();
// Items should remain in queue for retry
const pendingElement = page.locator('text=Pending:').locator('..');
await expect(pendingElement).toBeVisible();
// Remove route to allow successful retry
await page.unroute('**/api/scan/**');
// Trigger retry (would happen automatically in real implementation)
await page.waitForTimeout(2000);
});
test('should batch sync operations for efficiency', async ({ page }) => {
// Create multiple offline scans
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
});
// Enable optimistic scanning
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
if (!isEnabled) {await toggle.click();}
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
// Scan 10 items rapidly
for (let i = 1; i <= 10; i++) {
await page.evaluate((qr) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: { qr, timestamp: Date.now() }
}));
}, `TICKET_BATCH_${i.toString().padStart(3, '0')}`);
await page.waitForTimeout(100);
}
let batchRequestCount = 0;
await page.route('**/api/scan/batch', (route) => {
batchRequestCount++;
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
processed: 10,
successful: 10,
failed: 0
})
});
});
// Come online and sync
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
window.dispatchEvent(new Event('online'));
});
await page.waitForTimeout(5000);
// Should use batch API for efficiency
expect(batchRequestCount).toBeGreaterThan(0);
});
test('should maintain scan order during sync', async ({ page }) => {
// Create timestamped offline scans
await page.context().setOffline(true);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
});
const scanTimes = [];
for (let i = 1; i <= 5; i++) {
const scanTime = Date.now() + (i * 100);
scanTimes.push(scanTime);
await page.evaluate((data) => {
window.dispatchEvent(new CustomEvent('mock-scan', {
detail: {
qr: data.qr,
timestamp: data.timestamp
}
}));
}, { qr: `TICKET_ORDER_${i}`, timestamp: scanTime });
await page.waitForTimeout(150);
}
// Verify sync maintains chronological order
await page.route('**/api/scan/**', (route) => {
const body = route.request().postData();
if (body) {
const data = JSON.parse(body);
// Verify timestamp order is maintained
expect(data.timestamp).toBeDefined();
}
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true })
});
});
await page.context().setOffline(false);
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
window.dispatchEvent(new Event('online'));
});
await page.waitForTimeout(3000);
});
});