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 Badge Workflow Test * * Simulates the complete attendee badge check-in workflow: * 1. Navigate from home page to Event Badges * 2. Search for attendee by name (full name with space) * 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 * - Multi-word search functionality (tests "scott idem" split-word logic) * - Badge detail view rendering * - Quick edit feature (override fields) * - Save/cancel functionality * - Print button behavior (count increment, timestamp) * - Return navigation to badge list * * **STATUS: WIP** - Search results not displaying in test environment * - Multi-word search fix applied (splits "scott idem" → "scott" AND "idem") * - Test can use single word ("scott" or "idem") as workaround */ 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() === 'warning') 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 template (required for badge view to render) if (url.includes('/v3/crud/event_badge_template/') && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: 'template-1', event_badge_template_id: 'template-1', event_badge_template_id_random: 'template-1', id_random: 'template-1', event_id: event_id, event_id_random: event_id, name: 'Standard Badge Template', enable: '1', hide: '0' } }) }); } // 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_badge_id_random: badge_id, id_random: badge_id, event_id: event_id, event_id_random: 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 (V3 uses /v3/crud/event_badge/{id}, not nested under event) if (url.match(new RegExp(`/v3/crud/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_badge_id_random: badge_id, id_random: badge_id, event_id: event_id, event_id_random: 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_badge_id_random: badge_id, id_random: badge_id, event_id: event_id, event_id_random: 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 } ); }); test('Complete attendee badge workflow (navigate → edit → print → return)', async ({ page }) => { // Step 1: 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); // Step 2: Navigate directly to badge detail page console.log(`Navigating to /events/${event_id}/badges/${badge_id}`); await page.goto(`/events/${event_id}/badges/${badge_id}`); // Wait for badge to load and display await expect(page.getByRole('heading', { name: /Scott Idem/i })).toBeVisible({ timeout: 10000 }); console.log('✅ Badge detail page loaded'); // Step 3: Edit professional title using override field await page.locator('[data-testid="badge-edit-btn"]').click(); await page.waitForTimeout(300); const title_input = page.locator('[data-testid="badge-professional-title-input"]'); await title_input.waitFor({ state: 'visible', timeout: 3000 }); await title_input.fill('Lead Software Architect'); // Step 4: Save changes — wait for the PATCH request const save_promise = page.waitForRequest((r) => r.url().includes(`event_badge/${badge_id}`) && (r.method() === 'PATCH' || r.method() === 'PUT'), { timeout: 10000 } ); await page.locator('[data-testid="badge-save-btn"]').click(); const save_request = await save_promise; const post_json = JSON.parse(save_request.postData() ?? '{}'); expect(post_json.professional_title_override).toBe('Lead Software Architect'); await page.waitForTimeout(500); console.log('✅ Professional title override saved'); // Step 5: Print badge (increment print_count) const print_promise = page.waitForRequest((r) => r.url().includes(`event_badge/${badge_id}`) && (r.method() === 'PATCH' || r.method() === 'PUT'), { timeout: 10000 } ); await page.locator('[data-testid="badge-print-btn"]').click(); const print_request = await print_promise; const print_json = JSON.parse(print_request.postData() ?? '{}'); expect(print_json.print_count).toBe(1); expect(print_json.print_last_datetime).toBeDefined(); expect(print_json.print_first_datetime).toBeDefined(); console.log(`✅ Badge printed. Count: ${print_json.print_count}`); // After print, the page automatically navigates back to badge search await expect(page).toHaveURL(new RegExp(`/events/${event_id}/badges$`), { timeout: 5000 }); await expect(page.locator('#badge_fulltext_search_qry_str')).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 */ }); });