// @ts-check const { test, expect, devices } = require('@playwright/test'); /** * Comprehensive Ticket Purchasing Test Suite * * This test suite covers: * - End-to-end ticket purchasing flow * - Multiple ticket types and quantities * - Mobile responsive design * - Form validation and error handling * - Presale code functionality * - Inventory management and reservations * - Accessibility compliance * - Visual regression testing */ // Page Object Models class TicketPurchasePage { constructor(page) { this.page = page; // Main containers this.eventContainer = page.locator('[data-test="event-container"], .max-w-5xl'); this.ticketCheckout = page.locator('[data-test="ticket-checkout"]'); // Event details this.eventTitle = page.locator('h1').first(); this.eventDate = page.locator('[data-test="event-date"]').or(page.getByText(/Event Date/)); this.eventVenue = page.locator('[data-test="venue"]').or(page.getByText(/Venue/)); this.eventDescription = page.locator('[data-test="event-description"]'); // Presale code section this.presaleCodeSection = page.locator('[data-test="presale-code-section"]'); this.presaleCodeInput = page.locator('input#presale-code, input[placeholder*="presale"]'); this.presaleCodeButton = page.getByRole('button', { name: /apply code/i }); this.presaleCodeError = page.locator('[data-test="presale-error"]'); this.presaleCodeSuccess = page.locator('[data-test="presale-success"]'); // Ticket selection this.ticketTypes = page.locator('[data-test="ticket-type"]').or(page.locator('.border-2.rounded-2xl')); this.quantityDecrease = page.locator('button:has-text("−")'); this.quantityIncrease = page.locator('button:has-text("+")'); this.quantityDisplay = page.locator('[data-test="quantity-display"]'); // Order summary this.orderSummary = page.locator('[data-test="order-summary"]').or(page.getByText(/Order Summary/)); this.subtotalAmount = page.locator('[data-test="subtotal"]'); this.platformFeeAmount = page.locator('[data-test="platform-fee"]'); this.totalAmount = page.locator('[data-test="total"]'); // Customer information form this.emailInput = page.locator('input#email, input[type="email"]'); this.nameInput = page.locator('input#name, input[placeholder*="name"]'); this.purchaseButton = page.getByRole('button', { name: /complete purchase|purchase|checkout/i }); // Timer and reservations this.reservationTimer = page.locator('[data-test="reservation-timer"]'); this.timeRemaining = page.locator('[data-test="time-remaining"]'); // Loading and error states this.loadingIndicator = page.getByText(/loading/i); this.errorMessage = page.locator('[data-test="error-message"], .text-red-500'); this.successMessage = page.locator('[data-test="success-message"], .text-green-500'); } // Navigation methods async goto(eventSlug) { await this.page.goto(`/e/${eventSlug}`); await this.page.waitForLoadState('networkidle'); } // Presale code methods async enterPresaleCode(code) { await this.presaleCodeInput.fill(code); await this.presaleCodeButton.click(); await this.page.waitForTimeout(1000); // Wait for validation } async waitForPresaleValidation() { await expect(this.presaleCodeSuccess.or(this.presaleCodeError)).toBeVisible({ timeout: 5000 }); } // Ticket selection methods async selectTicketQuantity(ticketTypeIndex, quantity) { const ticketType = this.ticketTypes.nth(ticketTypeIndex); const increaseButton = ticketType.locator('button:has-text("+")'); const currentQuantity = await this.getCurrentQuantity(ticketTypeIndex); const difference = quantity - currentQuantity; if (difference > 0) { for (let i = 0; i < difference; i++) { await increaseButton.click(); await this.page.waitForTimeout(500); // Wait for reservation } } else if (difference < 0) { const decreaseButton = ticketType.locator('button:has-text("−")'); for (let i = 0; i < Math.abs(difference); i++) { await decreaseButton.click(); await this.page.waitForTimeout(500); } } } async getCurrentQuantity(ticketTypeIndex) { const ticketType = this.ticketTypes.nth(ticketTypeIndex); const quantityText = await ticketType.locator('.text-lg.font-semibold').textContent(); return parseInt(quantityText || '0'); } async getTicketTypePrice(ticketTypeIndex) { const ticketType = this.ticketTypes.nth(ticketTypeIndex); const priceText = await ticketType.locator('.text-xl, .text-2xl').filter({ hasText: '$' }).textContent(); return parseFloat((priceText || '$0').replace('$', '')); } async getTicketTypeName(ticketTypeIndex) { const ticketType = this.ticketTypes.nth(ticketTypeIndex); return await ticketType.locator('h3').textContent(); } // Form submission methods async fillCustomerInfo(email, name) { await this.emailInput.fill(email); await this.nameInput.fill(name); } async submitPurchase() { await this.purchaseButton.click(); } // Validation methods async waitForOrderSummary() { await expect(this.orderSummary).toBeVisible({ timeout: 5000 }); } async getTotalPrice() { const totalText = await this.totalAmount.textContent(); return parseFloat((totalText || '$0').replace('$', '')); } async waitForReservationTimer() { await expect(this.reservationTimer).toBeVisible({ timeout: 5000 }); } // Screenshot helpers async captureEventPage(filename) { await this.page.screenshot({ path: `screenshots/${filename}`, fullPage: true }); } async captureTicketSelection(filename) { await this.ticketCheckout.screenshot({ path: `screenshots/${filename}` }); } } // Test data setup const testEvents = { sample: { slug: 'sample-event', title: 'Sample Event', venue: 'Test Venue', ticketTypes: [ { name: 'General Admission', price: 25.00 }, { name: 'VIP', price: 75.00 } ] } }; const testCustomers = { valid: { email: 'test@blackcanyontickets.com', name: 'Test Customer' }, invalid: { email: 'invalid-email', name: '' } }; // Test Configuration const viewports = { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 } }; // Utility functions async function createScreenshotsDirectory() { const fs = require('fs'); if (!fs.existsSync('screenshots')) { fs.mkdirSync('screenshots', { recursive: true }); } } // Main Test Suite test.describe('Ticket Purchasing - Comprehensive Tests', () => { test.beforeEach(async ({ page }) => { await createScreenshotsDirectory(); // Set up consistent test environment await page.setViewportSize(viewports.desktop); // Mock API responses for testing await page.route('**/api/inventory/availability/*', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, availability: { available: 50, total: 100, reserved: 5, sold: 45, is_available: true } }) }); }); }); test.describe('Basic Ticket Purchasing Flow', () => { test('should display event information correctly', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); // Navigate to a test event (we'll use a mock or create one) await page.goto('/e/sample-event'); // Take initial screenshot await ticketPage.captureEventPage('event-page-initial.png'); // Verify event details are displayed await expect(ticketPage.eventTitle).toBeVisible(); await expect(ticketPage.eventDate).toBeVisible(); // Verify ticket checkout section is present await expect(ticketPage.ticketTypes.first()).toBeVisible(); console.log('✓ Event information displayed correctly'); }); test('should handle ticket selection and quantity changes', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); // Wait for ticket types to load await expect(ticketPage.ticketTypes.first()).toBeVisible(); // Select 2 tickets of the first type await ticketPage.selectTicketQuantity(0, 2); // Verify quantity is updated const quantity = await ticketPage.getCurrentQuantity(0); expect(quantity).toBe(2); // Take screenshot of ticket selection await ticketPage.captureTicketSelection('ticket-selection-2-tickets.png'); // Verify order summary appears await ticketPage.waitForOrderSummary(); // Verify reservation timer appears await ticketPage.waitForReservationTimer(); console.log('✓ Ticket selection and quantity changes work correctly'); }); test('should calculate pricing correctly', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await expect(ticketPage.ticketTypes.first()).toBeVisible(); // Get the price of the first ticket type const ticketPrice = await ticketPage.getTicketTypePrice(0); // Select 3 tickets await ticketPage.selectTicketQuantity(0, 3); await ticketPage.waitForOrderSummary(); // Verify total calculation (should include platform fees) const totalPrice = await ticketPage.getTotalPrice(); expect(totalPrice).toBeGreaterThan(ticketPrice * 3); console.log(`✓ Pricing calculated correctly: ${ticketPrice} × 3 = ${totalPrice} (including fees)`); }); test('should complete purchase flow with valid customer information', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); // Select tickets await expect(ticketPage.ticketTypes.first()).toBeVisible(); await ticketPage.selectTicketQuantity(0, 1); await ticketPage.waitForOrderSummary(); // Fill customer information await ticketPage.fillCustomerInfo(testCustomers.valid.email, testCustomers.valid.name); // Take screenshot before purchase await ticketPage.captureTicketSelection('pre-purchase-form-filled.png'); // Mock the purchase attempt API await page.route('**/api/inventory/purchase-attempt', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, purchase_attempt: { id: 'pa_test123', total_amount: 25.00, status: 'pending' } }) }); }); // Submit purchase await ticketPage.submitPurchase(); // Verify success or expected behavior // Note: The current implementation shows an alert, so we'll handle that await page.on('dialog', dialog => dialog.accept()); console.log('✓ Purchase flow completed successfully'); }); }); test.describe('Multiple Ticket Types and Quantities', () => { test('should handle multiple different ticket types', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await expect(ticketPage.ticketTypes.first()).toBeVisible(); // Select tickets from multiple types await ticketPage.selectTicketQuantity(0, 2); // General admission // If there's a second ticket type, select from it too const ticketCount = await ticketPage.ticketTypes.count(); if (ticketCount > 1) { await ticketPage.selectTicketQuantity(1, 1); // VIP } await ticketPage.waitForOrderSummary(); // Take screenshot of mixed ticket selection await ticketPage.captureTicketSelection('mixed-ticket-types-selection.png'); console.log('✓ Multiple ticket types handled correctly'); }); test('should enforce maximum quantity limits', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await expect(ticketPage.ticketTypes.first()).toBeVisible(); // Try to select many tickets (should be limited by availability) const increaseButton = ticketPage.ticketTypes.first().locator('button:has-text("+")'); // Click increase button multiple times for (let i = 0; i < 60; i++) { await increaseButton.click(); await page.waitForTimeout(100); // Check if button becomes disabled if (await increaseButton.isDisabled()) { break; } } const finalQuantity = await ticketPage.getCurrentQuantity(0); expect(finalQuantity).toBeLessThanOrEqual(50); // Based on our mock availability console.log(`✓ Quantity limits enforced: max ${finalQuantity} tickets`); }); }); test.describe('Mobile Responsive Design', () => { test('should work correctly on mobile devices', async ({ page }) => { await page.setViewportSize(viewports.mobile); const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); // Take mobile screenshot await ticketPage.captureEventPage('mobile-event-page.png'); // Verify elements are visible and accessible on mobile await expect(ticketPage.eventTitle).toBeVisible(); await expect(ticketPage.ticketTypes.first()).toBeVisible(); // Test ticket selection on mobile await ticketPage.selectTicketQuantity(0, 1); await ticketPage.waitForOrderSummary(); // Take mobile ticket selection screenshot await ticketPage.captureTicketSelection('mobile-ticket-selection.png'); // Test form filling on mobile await ticketPage.fillCustomerInfo(testCustomers.valid.email, testCustomers.valid.name); // Verify purchase button is accessible await expect(ticketPage.purchaseButton).toBeVisible(); console.log('✓ Mobile responsive design works correctly'); }); test('should work correctly on tablet devices', async ({ page }) => { await page.setViewportSize(viewports.tablet); const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await ticketPage.captureEventPage('tablet-event-page.png'); // Test core functionality on tablet await expect(ticketPage.ticketTypes.first()).toBeVisible(); await ticketPage.selectTicketQuantity(0, 2); await ticketPage.waitForOrderSummary(); await ticketPage.captureTicketSelection('tablet-ticket-selection.png'); console.log('✓ Tablet responsive design works correctly'); }); }); test.describe('Form Validation and Error Handling', () => { test('should validate email format', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await expect(ticketPage.ticketTypes.first()).toBeVisible(); await ticketPage.selectTicketQuantity(0, 1); await ticketPage.waitForOrderSummary(); // Enter invalid email await ticketPage.fillCustomerInfo(testCustomers.invalid.email, testCustomers.valid.name); // Try to submit await ticketPage.submitPurchase(); // Verify HTML5 validation prevents submission const isValidEmail = await ticketPage.emailInput.evaluate(el => el.validity.valid); expect(isValidEmail).toBe(false); console.log('✓ Email validation works correctly'); }); test('should require customer name', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); await expect(ticketPage.ticketTypes.first()).toBeVisible(); await ticketPage.selectTicketQuantity(0, 1); await ticketPage.waitForOrderSummary(); // Enter valid email but empty name await ticketPage.fillCustomerInfo(testCustomers.valid.email, ''); await ticketPage.submitPurchase(); // Verify required field validation const isValidName = await ticketPage.nameInput.evaluate(el => el.validity.valid); expect(isValidName).toBe(false); console.log('✓ Name requirement validation works correctly'); }); test('should handle sold out tickets gracefully', async ({ page }) => { // Mock sold out response await page.route('**/api/inventory/availability/*', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, availability: { available: 0, total: 100, reserved: 5, sold: 95, is_available: false } }) }); }); const ticketPage = new TicketPurchasePage(page); await page.goto('/e/sample-event'); // Verify sold out state is displayed await expect(page.getByText(/sold out/i)).toBeVisible({ timeout: 5000 }); // Verify increase button is disabled const increaseButton = ticketPage.ticketTypes.first().locator('button:has-text("+")'); await expect(increaseButton).toBeDisabled(); await ticketPage.captureTicketSelection('sold-out-state.png'); console.log('✓ Sold out state handled correctly'); }); test('should handle network errors gracefully', async ({ page }) => { const ticketPage = new TicketPurchasePage(page); // Mock network failure for availability await page.route('**/api/inventory/availability/*', async route => { await route.abort('failed'); }); await page.goto('/e/sample-event'); // Should show loading or error state await expect(ticketPage.loadingIndicator.or(ticketPage.errorMessage)).toBeVisible({ timeout: 5000 }); console.log('✓ Network errors handled gracefully'); }); }); test.describe('Presale Code Functionality', () => { test('should show presale code input when required', async ({ page }) => { // Mock event with presale requirement await page.route('**/e/presale-event', async route => { const mockEventPage = `