- 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>
627 lines
23 KiB
TypeScript
627 lines
23 KiB
TypeScript
/**
|
|
* 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');
|
|
}
|
|
});
|
|
}); |