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:
627
reactrebuild0825/tests/mobile-ux.spec.ts
Normal file
627
reactrebuild0825/tests/mobile-ux.spec.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
/**
|
||||
* Mobile UX Tests - Mobile-Specific User Experience Testing
|
||||
* Tests touch interactions, rotation handling, camera switching,
|
||||
* torch functionality, and permission flows for mobile devices
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Mobile Touch Interactions', () => {
|
||||
const testEventId = 'evt-001';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
|
||||
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}`);
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle touch interactions on all buttons', async ({ page }) => {
|
||||
// Test settings button touch
|
||||
const settingsButton = page.locator('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await expect(settingsButton).toBeVisible();
|
||||
|
||||
// Touch target should be large enough (minimum 44px)
|
||||
const buttonBox = await settingsButton.boundingBox();
|
||||
expect(buttonBox?.width).toBeGreaterThanOrEqual(44);
|
||||
expect(buttonBox?.height).toBeGreaterThanOrEqual(44);
|
||||
|
||||
// Touch interaction should work
|
||||
await settingsButton.tap();
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
|
||||
// Close settings with touch
|
||||
await settingsButton.tap();
|
||||
await expect(page.locator('text=Gate/Zone')).not.toBeVisible();
|
||||
|
||||
// Test torch button if present
|
||||
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
||||
if (await torchButton.isVisible()) {
|
||||
const torchBox = await torchButton.boundingBox();
|
||||
expect(torchBox?.width).toBeGreaterThanOrEqual(44);
|
||||
expect(torchBox?.height).toBeGreaterThanOrEqual(44);
|
||||
|
||||
await torchButton.tap();
|
||||
// Should handle torch toggle without errors
|
||||
}
|
||||
});
|
||||
|
||||
test('should provide tactile feedback for scan results', async ({ page }) => {
|
||||
// Test vibration API integration
|
||||
const vibrationSupported = await page.evaluate(() => 'vibrate' in navigator);
|
||||
|
||||
if (vibrationSupported) {
|
||||
// Simulate successful scan with vibration feedback
|
||||
await page.evaluate(() => {
|
||||
// Test vibration pattern for success
|
||||
navigator.vibrate([50, 30, 50]); // Short-medium-short pattern
|
||||
|
||||
window.dispatchEvent(new CustomEvent('mock-scan-result', {
|
||||
detail: {
|
||||
qr: 'VIBRATION_TEST_SUCCESS',
|
||||
status: 'success',
|
||||
message: 'Valid ticket',
|
||||
timestamp: Date.now(),
|
||||
vibrationPattern: [50, 30, 50]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Simulate error scan with different vibration
|
||||
await page.evaluate(() => {
|
||||
// Test vibration pattern for error
|
||||
navigator.vibrate([200]); // Single long vibration for error
|
||||
|
||||
window.dispatchEvent(new CustomEvent('mock-scan-result', {
|
||||
detail: {
|
||||
qr: 'VIBRATION_TEST_ERROR',
|
||||
status: 'error',
|
||||
message: 'Invalid ticket',
|
||||
timestamp: Date.now(),
|
||||
vibrationPattern: [200]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Scanner should remain functional regardless of vibration support
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle touch scrolling in settings panel', async ({ page }) => {
|
||||
// Open settings
|
||||
await page.locator('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)').tap();
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
|
||||
// Test scrolling behavior if settings panel has scrollable content
|
||||
const settingsPanel = page.locator('.settings-panel, [data-testid="settings-panel"]').first();
|
||||
|
||||
if (await settingsPanel.isVisible()) {
|
||||
// Try to scroll within settings panel
|
||||
await settingsPanel.hover();
|
||||
await page.mouse.wheel(0, 100);
|
||||
|
||||
// Should handle scroll without affecting main page
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
}
|
||||
|
||||
// Test input focus behavior
|
||||
const gateInput = page.locator('input[placeholder*="Gate"]');
|
||||
await gateInput.tap();
|
||||
await expect(gateInput).toBeFocused();
|
||||
|
||||
// Virtual keyboard shouldn't break layout
|
||||
await gateInput.fill('Gate A - Touch Test');
|
||||
await expect(gateInput).toHaveValue('Gate A - Touch Test');
|
||||
|
||||
// Dismiss keyboard by tapping outside
|
||||
await page.locator('h1').tap();
|
||||
await expect(gateInput).not.toBeFocused();
|
||||
});
|
||||
|
||||
test('should handle touch gestures and prevent zoom', async ({ page }) => {
|
||||
// Test that pinch-to-zoom is disabled for scanner area
|
||||
const videoElement = page.locator('video');
|
||||
await expect(videoElement).toBeVisible();
|
||||
|
||||
// Check viewport meta tag prevents zooming
|
||||
const viewportMeta = await page.locator('meta[name="viewport"]').getAttribute('content');
|
||||
expect(viewportMeta).toContain('user-scalable=no');
|
||||
|
||||
// Test touch events don't interfere with scanning
|
||||
const scannerArea = page.locator('.scanner-frame, [data-testid="scanner-frame"]').first();
|
||||
|
||||
if (await scannerArea.isVisible()) {
|
||||
// Simulate touch on scanner area
|
||||
await scannerArea.tap();
|
||||
|
||||
// Should not zoom or cause unwanted interactions
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
}
|
||||
|
||||
// Test swipe gestures don't interfere
|
||||
await page.touchscreen.tap(200, 300);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Scanner should remain stable
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Device Orientation Handling', () => {
|
||||
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 adapt layout for portrait orientation', async ({ page }) => {
|
||||
// Set portrait mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Video should be properly sized for portrait
|
||||
const video = page.locator('video');
|
||||
await expect(video).toBeVisible();
|
||||
|
||||
const videoBox = await video.boundingBox();
|
||||
expect(videoBox?.height).toBeGreaterThan(videoBox?.width || 0);
|
||||
|
||||
// Settings button should be accessible
|
||||
const settingsButton = page.locator('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await expect(settingsButton).toBeVisible();
|
||||
|
||||
// Stats area should be visible
|
||||
await expect(page.locator('text=Online')).toBeVisible();
|
||||
await expect(page.locator('text=Total:')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should adapt layout for landscape orientation', async ({ page }) => {
|
||||
// Set landscape mobile viewport
|
||||
await page.setViewportSize({ width: 667, height: 375 });
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Video should adapt to landscape
|
||||
const video = page.locator('video');
|
||||
await expect(video).toBeVisible();
|
||||
|
||||
const videoBox = await video.boundingBox();
|
||||
expect(videoBox?.width).toBeGreaterThan(videoBox?.height || 0);
|
||||
|
||||
// All critical elements should remain accessible in landscape
|
||||
await expect(page.locator('text=Online')).toBeVisible();
|
||||
|
||||
// Settings should still be accessible
|
||||
const settingsButton = page.locator('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
||||
await settingsButton.tap();
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
|
||||
// Settings panel should fit in landscape
|
||||
const gateInput = page.locator('input[placeholder*="Gate"]');
|
||||
await expect(gateInput).toBeVisible();
|
||||
const inputBox = await gateInput.boundingBox();
|
||||
expect(inputBox?.y).toBeGreaterThan(0); // Should be within viewport
|
||||
});
|
||||
|
||||
test('should handle orientation changes smoothly', async ({ page }) => {
|
||||
// Start in portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
|
||||
// Open settings in portrait
|
||||
await page.locator('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)').tap();
|
||||
await page.fill('input[placeholder*="Gate"]', 'Orientation Test');
|
||||
|
||||
// Rotate to landscape
|
||||
await page.setViewportSize({ width: 667, height: 375 });
|
||||
await page.waitForTimeout(1000); // Allow for orientation change
|
||||
|
||||
// Settings should remain open and functional
|
||||
await expect(page.locator('input[placeholder*="Gate"]')).toHaveValue('Orientation Test');
|
||||
await expect(page.locator('text=Gate/Zone')).toBeVisible();
|
||||
|
||||
// Video should still be working
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
|
||||
// Rotate back to portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Everything should still work
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
await expect(page.locator('input[placeholder*="Gate"]')).toHaveValue('Orientation Test');
|
||||
});
|
||||
|
||||
test('should lock orientation when supported', async ({ page }) => {
|
||||
// Test orientation lock API if available
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
const orientationLockSupported = await page.evaluate(async () => {
|
||||
if ('orientation' in screen && 'lock' in screen.orientation) {
|
||||
try {
|
||||
await screen.orientation.lock('portrait');
|
||||
return { supported: true, locked: true };
|
||||
} catch (error) {
|
||||
return { supported: true, locked: false, error: error.message };
|
||||
}
|
||||
}
|
||||
return { supported: false };
|
||||
});
|
||||
|
||||
console.log('Orientation lock support:', orientationLockSupported);
|
||||
|
||||
// Scanner should work regardless of orientation lock support
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Camera Switching and Controls', () => {
|
||||
const testEventId = 'evt-001';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
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 detect available cameras', async ({ page }) => {
|
||||
const cameraInfo = await page.evaluate(async () => {
|
||||
if ('mediaDevices' in navigator && 'enumerateDevices' in navigator.mediaDevices) {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const cameras = devices.filter(device => device.kind === 'videoinput');
|
||||
return {
|
||||
supported: true,
|
||||
cameraCount: cameras.length,
|
||||
cameras: cameras.map(camera => ({
|
||||
deviceId: camera.deviceId,
|
||||
label: camera.label,
|
||||
groupId: camera.groupId
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
return { supported: true, error: error.message };
|
||||
}
|
||||
}
|
||||
return { supported: false };
|
||||
});
|
||||
|
||||
console.log('Camera detection:', cameraInfo);
|
||||
|
||||
if (cameraInfo.supported && cameraInfo.cameraCount > 0) {
|
||||
expect(cameraInfo.cameraCount).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Scanner should work with whatever cameras are available
|
||||
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should switch between front and rear cameras', async ({ page }) => {
|
||||
// Check if camera switching UI exists
|
||||
const cameraSwitchButton = page.locator('button:has([data-testid="camera-switch"]), button:has(.lucide-camera-switch)');
|
||||
|
||||
if (await cameraSwitchButton.isVisible()) {
|
||||
// Test camera switching
|
||||
await cameraSwitchButton.tap();
|
||||
await page.waitForTimeout(2000); // Allow camera switch time
|
||||
|
||||
// Video should still be working after switch
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
|
||||
// Try switching back
|
||||
await cameraSwitchButton.tap();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
} else {
|
||||
// If no camera switch button, verify single camera works
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle camera constraints for mobile', async ({ page }) => {
|
||||
// Test mobile-specific camera constraints
|
||||
const cameraConstraints = await page.evaluate(async () => {
|
||||
if ('mediaDevices' in navigator) {
|
||||
try {
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
facingMode: 'environment', // Rear camera preferred for scanning
|
||||
frameRate: { ideal: 30, max: 60 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
const track = stream.getVideoTracks()[0];
|
||||
const settings = track.getSettings();
|
||||
|
||||
// Clean up
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
settings: {
|
||||
width: settings.width,
|
||||
height: settings.height,
|
||||
frameRate: settings.frameRate,
|
||||
facingMode: settings.facingMode
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
return { success: false, error: 'Media devices not supported' };
|
||||
});
|
||||
|
||||
console.log('Camera constraints test:', cameraConstraints);
|
||||
|
||||
// Scanner should work regardless of specific constraint support
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Torch/Flashlight Functionality', () => {
|
||||
const testEventId = 'evt-001';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
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 detect torch capability', async ({ page }) => {
|
||||
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const torchCapability = await page.evaluate(async () => {
|
||||
if ('mediaDevices' in navigator) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
const track = stream.getVideoTracks()[0];
|
||||
const capabilities = track.getCapabilities();
|
||||
|
||||
// Clean up
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
|
||||
return {
|
||||
supported: 'torch' in capabilities,
|
||||
capabilities: capabilities.torch || false
|
||||
};
|
||||
} catch (error) {
|
||||
return { supported: false, error: error.message };
|
||||
}
|
||||
}
|
||||
return { supported: false };
|
||||
});
|
||||
|
||||
console.log('Torch capability:', torchCapability);
|
||||
|
||||
// Check if torch button is visible based on capability
|
||||
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
||||
|
||||
if (torchCapability.supported) {
|
||||
// Should show torch button if supported
|
||||
await expect(torchButton).toBeVisible();
|
||||
}
|
||||
|
||||
// Scanner should work regardless of torch support
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should toggle torch on/off', async ({ page }) => {
|
||||
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
||||
|
||||
if (await torchButton.isVisible()) {
|
||||
// Test torch toggle
|
||||
await torchButton.tap();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify torch state change (visual indication would vary)
|
||||
// In a real test, you might check for class changes or aria-pressed attributes
|
||||
const isPressed = await torchButton.evaluate(el =>
|
||||
el.getAttribute('aria-pressed') === 'true' ||
|
||||
el.classList.contains('active') ||
|
||||
el.classList.contains('pressed')
|
||||
);
|
||||
|
||||
// Toggle back off
|
||||
await torchButton.tap();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should toggle state
|
||||
const isPressedAfter = await torchButton.evaluate(el =>
|
||||
el.getAttribute('aria-pressed') === 'true' ||
|
||||
el.classList.contains('active') ||
|
||||
el.classList.contains('pressed')
|
||||
);
|
||||
|
||||
expect(isPressed).not.toBe(isPressedAfter);
|
||||
}
|
||||
|
||||
// Scanner functionality should not be affected
|
||||
await expect(page.locator('video')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should provide visual feedback for torch state', async ({ page }) => {
|
||||
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
||||
|
||||
if (await torchButton.isVisible()) {
|
||||
// Get initial visual state
|
||||
const initialClasses = await torchButton.getAttribute('class');
|
||||
const initialAriaPressed = await torchButton.getAttribute('aria-pressed');
|
||||
|
||||
// Toggle torch
|
||||
await torchButton.tap();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Check for visual changes
|
||||
const afterClasses = await torchButton.getAttribute('class');
|
||||
const afterAriaPressed = await torchButton.getAttribute('aria-pressed');
|
||||
|
||||
// Should have some visual indication of state change
|
||||
const hasVisualChange =
|
||||
initialClasses !== afterClasses ||
|
||||
initialAriaPressed !== afterAriaPressed;
|
||||
|
||||
expect(hasVisualChange).toBe(true);
|
||||
|
||||
// Button should still be accessible
|
||||
const buttonBox = await torchButton.boundingBox();
|
||||
expect(buttonBox?.width).toBeGreaterThanOrEqual(44);
|
||||
expect(buttonBox?.height).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Permission Flows', () => {
|
||||
const testEventId = 'evt-001';
|
||||
|
||||
test('should handle camera permission denied gracefully', async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Don't grant camera 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');
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
|
||||
// Should show permission request or error message
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Scanner should still load the UI
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Should show appropriate error state
|
||||
// (Implementation would show permission denied message or retry button)
|
||||
|
||||
// Check if there's a retry/permission button
|
||||
const permissionButton = page.locator('button:has-text("Grant Camera Permission"), button:has-text("Retry"), button:has-text("Enable Camera")');
|
||||
if (await permissionButton.isVisible()) {
|
||||
await permissionButton.tap();
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
});
|
||||
|
||||
test('should request permissions on first visit', async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Clear all permissions first
|
||||
await context.clearPermissions();
|
||||
|
||||
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 - should trigger permission request
|
||||
await page.goto(`/scan?eventId=${testEventId}`);
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Grant permission when prompted
|
||||
await context.grantPermissions(['camera']);
|
||||
|
||||
// Should eventually show video
|
||||
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should handle notification permissions for alerts', async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
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 notification permission request
|
||||
const notificationPermission = await page.evaluate(async () => {
|
||||
if ('Notification' in window) {
|
||||
const permission = await Notification.requestPermission();
|
||||
return { supported: true, permission };
|
||||
}
|
||||
return { supported: false };
|
||||
});
|
||||
|
||||
console.log('Notification permission:', notificationPermission);
|
||||
|
||||
if (notificationPermission.supported) {
|
||||
expect(['granted', 'denied', 'default']).toContain(notificationPermission.permission);
|
||||
}
|
||||
|
||||
// Scanner should work regardless of notification permission
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show helpful permission instructions', async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Start without camera permission
|
||||
await context.clearPermissions();
|
||||
|
||||
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}`);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should show instructions or help text about camera permissions
|
||||
// (Implementation would show helpful guidance)
|
||||
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
||||
|
||||
// Look for permission-related text
|
||||
const hasPermissionGuidance = await page.evaluate(() => {
|
||||
const text = document.body.innerText.toLowerCase();
|
||||
return text.includes('camera') && (
|
||||
text.includes('permission') ||
|
||||
text.includes('access') ||
|
||||
text.includes('allow') ||
|
||||
text.includes('enable')
|
||||
);
|
||||
});
|
||||
|
||||
// Should provide some guidance about permissions
|
||||
if (!hasPermissionGuidance) {
|
||||
console.log('Note: Consider adding permission guidance text for better UX');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user