- 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>
359 lines
14 KiB
TypeScript
359 lines
14 KiB
TypeScript
import path from 'path';
|
|
import { test, expect } from '@playwright/test';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* Publish-Scanner Smoke Tests
|
|
*
|
|
* Critical flow validation for:
|
|
* - Event navigation and detail page access
|
|
* - Publish logic based on payment connection status (org.payment.connected)
|
|
* - Scanner navigation and UI rendering with camera mocking
|
|
* - Error state handling and blocked publish flows
|
|
* - Screenshots for visual validation
|
|
*
|
|
* This test suite is designed for CI environments with worker count 1
|
|
* and does not require sudo permissions.
|
|
*/
|
|
|
|
const DEMO_ACCOUNTS = {
|
|
admin: { email: 'admin@example.com', password: 'demo123' }, // org_001, payment connected
|
|
organizer: { email: 'organizer@example.com', password: 'demo123' }, // org_002, payment NOT connected
|
|
};
|
|
|
|
async function takeScreenshot(page: Page, name: string) {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const fileName = `publish_scanner_${name}_${timestamp}.png`;
|
|
await page.screenshot({
|
|
path: path.join('screenshots', fileName),
|
|
fullPage: true
|
|
});
|
|
return fileName;
|
|
}
|
|
|
|
async function mockCameraForScanner(page: Page) {
|
|
// Mock camera API for scanner testing without requiring real camera access
|
|
await page.addInitScript(() => {
|
|
// Mock getUserMedia for camera testing
|
|
Object.defineProperty(navigator, 'mediaDevices', {
|
|
writable: true,
|
|
value: {
|
|
getUserMedia: () => Promise.resolve({
|
|
getTracks: () => [],
|
|
addTrack: () => {},
|
|
removeTrack: () => {},
|
|
getVideoTracks: () => [],
|
|
getAudioTracks: () => [],
|
|
}),
|
|
enumerateDevices: () => Promise.resolve([{
|
|
deviceId: 'mock-camera',
|
|
kind: 'videoinput',
|
|
label: 'Mock Camera',
|
|
groupId: 'mock-group'
|
|
}])
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loginAs(page: Page, userType: 'admin' | 'organizer') {
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Use demo button method (same as smoke.spec.ts) for reliable login
|
|
if (userType === 'admin') {
|
|
await page.click('[data-testid="demo-user-sarah-admin"]');
|
|
} else {
|
|
await page.click('[data-testid="demo-user-john-organizer"]');
|
|
}
|
|
|
|
// Submit form
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for dashboard redirect
|
|
await expect(page).toHaveURL('/dashboard', { timeout: 10000 });
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await takeScreenshot(page, `logged-in-as-${userType}`);
|
|
}
|
|
|
|
async function navigateToFirstEvent(page: Page): Promise<string> {
|
|
// Navigate to events page
|
|
await page.goto('/events');
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, 'events-page-loaded');
|
|
|
|
// Click first event card to go to detail page
|
|
const firstEventCard = page.locator('[data-testid="event-card"]').first();
|
|
await expect(firstEventCard).toBeVisible({ timeout: 10000 });
|
|
await firstEventCard.click();
|
|
|
|
// Verify we're on event detail page and get event ID from URL
|
|
await expect(page.locator('[data-testid="event-detail-page"]')).toBeVisible({ timeout: 10000 });
|
|
await takeScreenshot(page, 'event-detail-page-loaded');
|
|
|
|
// Extract event ID from URL
|
|
const currentUrl = page.url();
|
|
const eventIdMatch = currentUrl.match(/\/events\/([^\/\?]+)/);
|
|
const eventId = eventIdMatch?.[1];
|
|
|
|
expect(eventId).toBeTruthy();
|
|
return eventId!;
|
|
}
|
|
|
|
test.describe('Publish-Scanner Smoke Tests', () => {
|
|
|
|
test('admin user - payment connected - can publish and access scanner', async ({ page }) => {
|
|
// Login as admin (org_001, payment connected)
|
|
await loginAs(page, 'admin');
|
|
|
|
// Navigate to first event detail page
|
|
const eventId = await navigateToFirstEvent(page);
|
|
|
|
// Verify EventDetail header is visible
|
|
await expect(page.locator('h1')).toBeVisible();
|
|
await expect(page.locator('[data-testid="event-detail-page"]')).toBeVisible();
|
|
|
|
// Test publish logic - admin should be able to publish (payment connected)
|
|
const publishButton = page.locator('[data-testid="publishBtn"], [data-testid="publish-event-button"]').first();
|
|
|
|
if (await publishButton.isVisible()) {
|
|
// If publish button exists, it should be enabled (not blocked by payment)
|
|
await expect(publishButton).toBeEnabled();
|
|
await takeScreenshot(page, 'admin-publish-button-enabled');
|
|
|
|
// Optional: Click to open publish modal and verify no payment blocking
|
|
await publishButton.click();
|
|
|
|
// Look for publish modal
|
|
const publishModal = page.locator('text=Publish Event').first();
|
|
if (await publishModal.isVisible()) {
|
|
await takeScreenshot(page, 'admin-publish-modal-open');
|
|
|
|
// Close modal with escape
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
} else {
|
|
// Event might already be published
|
|
await takeScreenshot(page, 'admin-event-already-published');
|
|
}
|
|
|
|
// Navigate to scanner with camera mocking
|
|
await mockCameraForScanner(page);
|
|
await page.goto(`/scan?eventId=${eventId}`);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Confirm scanner UI renders
|
|
await expect(page.locator('[data-testid="scanner-interface"]')).toBeVisible({ timeout: 15000 });
|
|
await takeScreenshot(page, 'admin-scanner-ui-loaded');
|
|
|
|
// Verify scanner has basic elements
|
|
await expect(page.locator('text=Scanner')).toBeVisible();
|
|
|
|
// Test that camera functionality doesn't crash (mocked)
|
|
console.log('✅ Scanner UI loaded successfully with mocked camera for admin user');
|
|
});
|
|
|
|
test('organizer user - payment NOT connected - publish blocked', async ({ page }) => {
|
|
// Login as organizer (org_002, payment NOT connected)
|
|
await loginAs(page, 'organizer');
|
|
|
|
// Navigate to first event detail page
|
|
const eventId = await navigateToFirstEvent(page);
|
|
|
|
// Verify EventDetail header is visible
|
|
await expect(page.locator('h1')).toBeVisible();
|
|
await expect(page.locator('[data-testid="event-detail-page"]')).toBeVisible();
|
|
|
|
// Test publish logic - organizer should be blocked by payment connection
|
|
const publishButton = page.locator('[data-testid="publishBtn"], [data-testid="publish-event-button"]').first();
|
|
|
|
if (await publishButton.isVisible()) {
|
|
// Publish button should exist but lead to blocking when clicked
|
|
await publishButton.click();
|
|
await takeScreenshot(page, 'organizer-publish-button-clicked');
|
|
|
|
// Look for publish modal
|
|
const publishModal = page.locator('text=Publish Event').first();
|
|
if (await publishModal.isVisible()) {
|
|
// Verify payment processing requirement is failing
|
|
const paymentCheck = page.locator('text=Payment Processing').locator('..');
|
|
await expect(paymentCheck.locator('[aria-label="Failed"]')).toBeVisible();
|
|
|
|
// Verify publish button in modal is disabled
|
|
const modalPublishButton = page.locator('button:has-text("Publish Event")');
|
|
await expect(modalPublishButton).toBeDisabled();
|
|
|
|
await takeScreenshot(page, 'organizer-publish-blocked-by-payment');
|
|
|
|
// Close modal
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
}
|
|
|
|
// Verify payment banner is visible (organizer has disconnected payment)
|
|
const paymentBanner = page.locator('[data-testid="payment-banner"]');
|
|
if (await paymentBanner.isVisible()) {
|
|
await expect(paymentBanner).toBeVisible();
|
|
await takeScreenshot(page, 'organizer-payment-banner-visible');
|
|
}
|
|
|
|
// Navigate to scanner (should still work regardless of payment status)
|
|
await mockCameraForScanner(page);
|
|
await page.goto(`/scan?eventId=${eventId}`);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Confirm scanner UI renders (scanner access not dependent on payment)
|
|
await expect(page.locator('[data-testid="scanner-interface"]')).toBeVisible({ timeout: 15000 });
|
|
await takeScreenshot(page, 'organizer-scanner-ui-loaded');
|
|
|
|
// Verify scanner has basic elements
|
|
await expect(page.locator('text=Scanner')).toBeVisible();
|
|
|
|
// Verify scanner works despite payment disconnection
|
|
console.log('✅ Scanner UI accessible for organizer despite payment disconnection');
|
|
});
|
|
|
|
test('scanner UI has essential components', async ({ page }) => {
|
|
// Login as admin for this scanner-focused test
|
|
await loginAs(page, 'admin');
|
|
|
|
// Navigate to events and get first event ID
|
|
const eventId = await navigateToFirstEvent(page);
|
|
|
|
// Navigate directly to scanner with camera mocking
|
|
await mockCameraForScanner(page);
|
|
await page.goto(`/scan?eventId=${eventId}`);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Wait for scanner interface to load
|
|
await expect(page.locator('[data-testid="scanner-interface"]')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Check for manual entry functionality (from QR system tests)
|
|
const manualEntryButton = page.locator('button[title="Manual Entry"]');
|
|
if (await manualEntryButton.isVisible()) {
|
|
await expect(manualEntryButton).toBeVisible();
|
|
await takeScreenshot(page, 'scanner-manual-entry-available');
|
|
|
|
// Test opening manual entry modal
|
|
await manualEntryButton.click();
|
|
await expect(page.locator('h2:text("Manual Entry")')).toBeVisible();
|
|
await takeScreenshot(page, 'scanner-manual-entry-modal');
|
|
|
|
// Close modal
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
|
|
// Check for scanner instructions or guidance text
|
|
const instructionsVisible = await page.locator('text*=scanner', 'text*=QR', 'text*=camera').first().isVisible();
|
|
expect(instructionsVisible).toBeTruthy();
|
|
|
|
await takeScreenshot(page, 'scanner-complete-ui-check');
|
|
});
|
|
|
|
test('event navigation flow validation', async ({ page }) => {
|
|
// Test the complete flow without focusing on specific payment status
|
|
await loginAs(page, 'admin');
|
|
|
|
// Start from dashboard, navigate to events
|
|
await expect(page).toHaveURL('/dashboard');
|
|
|
|
// Go to events page
|
|
await page.goto('/events');
|
|
await expect(page.locator('h1')).toContainText('Events');
|
|
await takeScreenshot(page, 'events-page-header-check');
|
|
|
|
// Verify events are loaded (either cards or empty state)
|
|
await expect(page.locator('[data-testid="events-container"]')).toBeVisible();
|
|
|
|
// Check if we have any event cards
|
|
const eventCards = page.locator('[data-testid="event-card"]');
|
|
const cardCount = await eventCards.count();
|
|
|
|
if (cardCount > 0) {
|
|
// Click first event card
|
|
await eventCards.first().click();
|
|
|
|
// Verify navigation to event detail
|
|
await expect(page).toHaveURL(/\/events\/[^\/]+$/);
|
|
await expect(page.locator('[data-testid="event-detail-page"]')).toBeVisible();
|
|
await takeScreenshot(page, 'navigation-flow-complete');
|
|
} else {
|
|
// No events available - this is also a valid state to test
|
|
await takeScreenshot(page, 'events-page-empty-state');
|
|
}
|
|
});
|
|
|
|
test('scanner direct access with event ID', async ({ page }) => {
|
|
// Test direct scanner access without going through event detail page
|
|
await loginAs(page, 'admin');
|
|
|
|
// Use a known event ID (from mock data) or get from events page
|
|
const eventId = await navigateToFirstEvent(page);
|
|
|
|
// Navigate directly to scanner with eventId parameter and camera mocking
|
|
await mockCameraForScanner(page);
|
|
await page.goto(`/scan?eventId=${eventId}`);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify scanner loads with event context
|
|
await expect(page.locator('[data-testid="scanner-interface"]')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Verify the URL contains the event ID
|
|
expect(page.url()).toContain(`eventId=${eventId}`);
|
|
|
|
await takeScreenshot(page, 'scanner-direct-access-with-event-id');
|
|
|
|
console.log('✅ Direct scanner access with event ID parameter works correctly');
|
|
});
|
|
|
|
test('payment connection status validation', async ({ page }) => {
|
|
// Test both payment connected and disconnected scenarios
|
|
|
|
// Part 1: Admin with payment connected
|
|
await loginAs(page, 'admin');
|
|
const eventId = await navigateToFirstEvent(page);
|
|
|
|
// Check for payment status indicators on event detail page
|
|
const paymentConnectedIndicator = page.locator('[data-testid="payment-status-connected"]');
|
|
const paymentDisconnectedBanner = page.locator('[data-testid="payment-banner"]');
|
|
|
|
// Admin should have payment connected, so no disconnect banner
|
|
const hasBanner = await paymentDisconnectedBanner.isVisible();
|
|
if (!hasBanner) {
|
|
console.log('✅ Admin user: No payment disconnection banner (payment connected)');
|
|
await takeScreenshot(page, 'admin-no-payment-banner');
|
|
}
|
|
|
|
// Logout and test organizer
|
|
await page.click('[data-testid="user-menu"]');
|
|
await page.click('[data-testid="logout-button"]');
|
|
await expect(page).toHaveURL('/login', { timeout: 10000 });
|
|
|
|
// Part 2: Organizer with payment NOT connected
|
|
await loginAs(page, 'organizer');
|
|
await navigateToFirstEvent(page);
|
|
|
|
// Organizer should see payment banner or blocking
|
|
const organizerBanner = await paymentDisconnectedBanner.isVisible();
|
|
if (organizerBanner) {
|
|
console.log('✅ Organizer user: Payment disconnection banner visible');
|
|
await takeScreenshot(page, 'organizer-payment-status-check');
|
|
}
|
|
|
|
// Try to access publish modal if available
|
|
const publishButton = page.locator('[data-testid="publishBtn"], [data-testid="publish-event-button"]').first();
|
|
if (await publishButton.isVisible()) {
|
|
await publishButton.click();
|
|
|
|
// Look for payment processing check
|
|
const paymentCheckFailed = page.locator('text=Payment Processing').locator('..').locator('[aria-label="Failed"]');
|
|
if (await paymentCheckFailed.isVisible()) {
|
|
console.log('✅ Publish blocked by payment status for organizer');
|
|
await takeScreenshot(page, 'payment-status-blocking-publish');
|
|
}
|
|
|
|
await page.keyboard.press('Escape');
|
|
}
|
|
});
|
|
}); |