import { test, expect } from '@playwright/test'; import { ae_app_local_data_defaults } from './_helpers/ae_defaults'; import { testing_event_id, testing_account_id, testing_person_id, mock_site_domain } from './_helpers/env'; const event_id = testing_event_id; const badge_id = 'test-badge-attendee-1'; /** * Attendee BadgeWorkflow Test * * **STATUS: WORK IN PROGRESS - Debugging search results display** * * Simulates the complete attendee badge check-in workflow: * 1. Navigate from home page to Event Badges * 2. Search for attendee by name * 3. Click badge to view details * 4. Edit professional_title using override field * 5. Save changes * 6. Increment print count (simulate printing badge) * 7. Return to badge search for next attendee * * This test validates: * - Navigation flow from home → badges * - Search functionality (fulltext) * - Badge detail view rendering * - Quick edit feature (override fields) * - Save/cancel functionality * - Print button behavior (count increment, timestamp) * - Return navigation to badge list */ test.describe('Event Badge - Attendee Workflow', () => { test.beforeEach(async ({ page }) => { // Error logging page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); page.on('console', (msg) => { if (msg.type() === 'error' || msg.type() === 'warn') console.error(`BROWSER [${msg.type().toUpperCase()}]: ${msg.text()}`); }); // Mock V3 API responses await page.route('**/v3/**', async (route) => { const req = route.request(); const url = req.url(); const method = req.method(); // Site domain lookup (prevents "Domain Not Registered" overlay) if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_site_domain] }) }); } // Event object if (url.includes(`/v3/crud/event/${event_id}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: event_id, event_id: event_id, name: 'Demo One Sky IT Conference', cfg_json: {}, mod_pres_mgmt_json: {}, mod_badges_json: {}, mod_abstracts_json: {}, mod_exhibits_json: {}, mod_meetings_json: {} } }) }); } // Badge search (returns test attendee) if (url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [{ id: badge_id, event_badge_id: badge_id, event_id: event_id, event_badge_template_id: 'template-1', given_name: 'Scott', family_name: 'Idem', full_name: 'Scott Idem', full_name_override: null, professional_title: 'Software Developer', professional_title_override: null, affiliations: 'One Sky IT', affiliations_override: null, email: 'scott@oneskyit.com', email_override: null, location: 'Seattle, WA', location_override: null, badge_type: 'member', badge_type_code: 'current_member', badge_type_override: null, badge_type_code_override: null, print_count: 0, print_first_datetime: null, print_last_datetime: null, default_qry_str: 'scott idem scott@oneskyit.com', enable: '1', hide: '0', created_on: '2026-02-01T10:00:00Z', updated_on: '2026-02-01T10:00:00Z' }] }) }); } // Badge single object GET if (url.match(new RegExp(`/v3/crud/event/${event_id}/event_badge/${badge_id}`)) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: badge_id, event_badge_id: badge_id, event_id: event_id, event_badge_template_id: 'template-1', given_name: 'Scott', family_name: 'Idem', full_name: 'Scott Idem', full_name_override: null, professional_title: 'Software Developer', professional_title_override: null, affiliations: 'One Sky IT', affiliations_override: null, email: 'scott@oneskyit.com', email_override: null, location: 'Seattle, WA', location_override: null, badge_type: 'member', badge_type_code: 'current_member', badge_type_override: null, badge_type_code_override: null, print_count: 0, print_first_datetime: null, print_last_datetime: null, default_qry_str: 'scott idem scott@oneskyit.com', enable: '1', hide: '0', created_on: '2026-02-01T10:00:00Z', updated_on: '2026-02-01T10:00:00Z' } }) }); } // Badge PATCH/PUT (update) if (url.match(new RegExp(`/v3/crud/event/${event_id}/event_badge/${badge_id}`)) && (method === 'PATCH' || method === 'PUT')) { const post_data = await req.postData(); const body = post_data ? JSON.parse(post_data) : {}; return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: badge_id, event_badge_id: badge_id, event_id: event_id, event_badge_template_id: 'template-1', given_name: 'Scott', family_name: 'Idem', full_name: 'Scott Idem', full_name_override: body.full_name_override ?? null, professional_title: 'Software Developer', professional_title_override: body.professional_title_override ?? null, affiliations: 'One Sky IT', affiliations_override: body.affiliations_override ?? null, email: 'scott@oneskyit.com', email_override: body.email_override ?? null, location: 'Seattle, WA', location_override: body.location_override ?? null, badge_type: 'member', badge_type_code: body.badge_type_code ?? 'current_member', badge_type_override: body.badge_type_override ?? null, badge_type_code_override: body.badge_type_code_override ?? null, print_count: body.print_count ?? 0, print_first_datetime: body.print_first_datetime ?? null, print_last_datetime: body.print_last_datetime ?? null, default_qry_str: 'scott idem scott@oneskyit.com', enable: '1', hide: '0', created_on: '2026-02-01T10:00:00Z', updated_on: new Date().toISOString() } }) }); } // Default: empty envelope return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); }); // Set up environment with authenticated access await page.addInitScript( ({ defaults, event_id, account_id, person_id }) => { const test_data = { ...defaults, account_id: account_id, authenticated_access: true, trusted_access: true, edit_mode: true, person_id: person_id, user: { id: person_id }, mod: { ...defaults.mod, events: { ...defaults.mod.events, event_id: event_id, badges: { qry__remote_first: true, // Force API search instead of local IDB first fulltext_search_qry_str: '', search_version: 0 } } } }; window.localStorage.setItem('ae_loc', JSON.stringify(test_data)); }, { defaults: ae_app_local_data_defaults, event_id: event_id, account_id: testing_account_id, person_id: testing_person_id } ); // Navigate to home page and delete IndexedDB for cold-start await page.goto('/'); await page.evaluate(() => { const dbs = ['ae_events_db', 'ae_core_db']; return Promise.all( dbs.map((name) => new Promise((resolve) => { try { const req = indexedDB.deleteDatabase(name); req.onsuccess = () => resolve(true); req.onerror = () => resolve(false); req.onblocked = () => resolve(false); } catch (e) { resolve(false); } }) ) ); }); // Wait a moment for IndexedDB to be ready await page.waitForTimeout(500); }); test('Complete attendee check-in workflow: navigate → search → view → edit → print → return', async ({ page }) => { // Step 1: Start at home page await page.goto('/'); await expect(page).toHaveTitle(/OSIT/); // Wait for page to be ready (no "Domain Not Registered" overlay) await expect(page.locator('text=Domain Not Registered')).not.toBeVisible({ timeout: 3000 }); // Step 2: Navigate to Event Badges page // In a real app, user would click through navigation. For test, go direct to badges page. await page.goto(`/events/${event_id}/badges`); // Wait for badges page to load (search input is a good indicator) const search_input = page.locator('input[type="search"], input[placeholder*="Search"]').first(); // Step 3: Search for attendee by name await search_input.waitFor({ state: 'visible', timeout: 5000 }); // Debug: Check what's on the page before searching const results_header_before = await page.locator('text=/Results:/i').count(); console.log(`Results header before search: ${results_header_before}`); await search_input.fill('Scott Idem'); await search_input.press('Enter'); // Wait for search to complete (longer timeout for API call + IDB save) await page.waitForTimeout(2500); // Debug: Take screenshot and dump HTML await page.screenshot({ path: 'test-results/badge-search-results.png', fullPage: true }); const page_content = await page.content(); console.log('=== Page HTML (first 2000 chars) ==='); console.log(page_content.substring(0, 2000)); if (!page_content.includes('Scott')) { console.log('⚠️ Search results may not have loaded. Page content check failed.'); // Check if there's an error message or loading state const loading_indicator = await page.locator('text=/Loading|Searching/i').count(); const error_msg = await page.locator('text=/Error|Failed/i').count(); console.log(`Loading indicators: ${loading_indicator}, Error messages: ${error_msg}`); } // Step 4: Click on the badge to view details // Badge link is an anchor with full name text const badge_link = page.locator('a[href*="/badges/"]').filter({ hasText: /Scott Idem/i }).first(); await badge_link.waitFor({ state: 'visible', timeout: 10000 }); await badge_link.click(); // Wait for badge detail page await expect(page).toHaveURL(new RegExp(`/events/${event_id}/badges/${badge_id}`), { timeout: 5000 }); // Verify badge details are visible await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 5000 }); await expect(page.locator('text=Software Developer')).toBeVisible({ timeout: 5000 }); // Step 5: Edit professional title using override field // Look for edit button or quick edit mode const edit_button = page.getByRole('button', { name: /Edit|Quick Edit/i }); if (await edit_button.isVisible({ timeout: 2000 })) { await edit_button.click(); } else { // Check if already in edit mode (review page or #review hash) console.log('Already in edit mode or no edit button found'); } // Find professional title input (may be labeled "Professional Title Override") const title_input = page.locator('input[type="text"]').filter({ has: page.locator('.. >> text=/Professional Title/i') }).or( page.locator('label:has-text("Professional Title")').locator('input') ).first(); await title_input.waitFor({ state: 'visible', timeout: 5000 }); await title_input.clear(); await title_input.fill('Lead Software Architect'); // Step 6: Save changes const save_button = page.getByRole('button', { name: /Save|Update/i }); const [update_request] = await Promise.all([ page.waitForRequest((r) => r.url().includes(`/v3/crud/event/${event_id}/event_badge/${badge_id}`) && (r.method() === 'PATCH' || r.method() === 'PUT'), { timeout: 10000 } ), save_button.click() ]); // Verify the save request included the override field const post_body = update_request.postData(); const post_json = post_body ? JSON.parse(post_body) : {}; expect(post_json.professional_title_override).toBe('Lead Software Architect'); // Wait for save to complete await expect(page.locator('text=Lead Software Architect')).toBeVisible({ timeout: 5000 }); // Step 7: Simulate print button (increment print_count) // For now, we'll just verify the UI shows print count = 0 // TODO: Add onclick handler to print button that increments count const print_section = page.locator('text=/Print Count|Printed/i'); if (await print_section.isVisible({ timeout: 2000 })) { await expect(print_section).toContainText('0'); console.log('Print count verified: 0 (print button not yet implemented)'); } // Step 8: Return to badge search for next attendee const back_button = page.getByRole('button', { name: /Back|Return|Badge/i }).or( page.getByRole('link', { name: /Badge/i }) ).first(); if (await back_button.isVisible({ timeout: 2000 })) { await back_button.click(); await expect(page).toHaveURL(new RegExp(`/events/${event_id}/badges`), { timeout: 5000 }); } else { // Navigate directly back await page.goto(`/events/${event_id}/badges`); } // Verify we're back at the badge search page await expect(page.locator('input[type="search"], input[placeholder*="Search"]')).toBeVisible({ timeout: 5000 }); console.log('✅ Returned to badge search - ready for next attendee'); }); test('Future: Attendee review feature (unauthenticated email link)', async ({ page }) => { // This test documents the future "Review" workflow // where attendees receive an email link to review/edit their badge // before arrival or while waiting in line test.skip(); /* * Future workflow: * 1. Attendee searches for their name (unauthenticated) * 2. Results show "Send Review Link" button instead of direct edit * 3. Click button → email sent with secure link * 4. Attendee clicks link → /events/{event_id}/badges/{badge_id}/review * 5. Can view and edit allowed fields (full_name, professional_title, affiliations, location) * 6. Save changes → override fields updated * 7. Changes protected from automated sync overwrites * * Security: * - Time-limited token in URL (expires after 24-48 hours) * - One-time use or limited use * - Only allowed fields editable (no email, badge_type) * - Audit log of self-service changes */ }); });