import { test, expect } from '@playwright/test'; import { testing_event_id } from './_helpers/env'; import { testing_exhibit_id, exhibit_staff_passcode, setup_leads_test_page, minimal_badge, } from './_helpers/leads_helpers'; const event_id = testing_event_id; const exhibit_id = testing_exhibit_id; const exhibit_url = `/events/${event_id}/leads/exhibit/${exhibit_id}`; // Pre-seeded auth used by all tests in this suite const signed_in_kv = { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' } }; // Seed leads_overrides to start in search mode (not QR). // Without this, the add tab shows the QR scanner which cannot be exercised in Playwright. const search_mode_seed = { tab_add_mode: { [exhibit_id]: 'search' } }; test.describe('Leads — Add Lead (manual search)', () => { // ----------------------------------------------------------------------- // 1. Navigate to Add tab and see search form // ----------------------------------------------------------------------- test('clicking Add Lead shows manual search form', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, leads_overrides: search_mode_seed, }); await page.goto(exhibit_url); // Click "Add Lead" header button to enter the add tab await page.locator('header button.preset-filled-primary').click(); // Manual search form must be visible await expect( page.locator('input[placeholder="Attendee name, email, or badge ID..."]') ).toBeVisible({ timeout: 8_000 }); }); // ----------------------------------------------------------------------- // 2. Search returns results // ----------------------------------------------------------------------- test('badge search returns results and shows Add button for opted-in attendees', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, leads_overrides: search_mode_seed, // badge_li defaults to [minimal_badge()] — one badge with allow_tracking=true }); await page.goto(exhibit_url); await page.locator('header button.preset-filled-primary').click(); const search_input = page.locator( 'input[placeholder="Attendee name, email, or badge ID..."]' ); await expect(search_input).toBeVisible({ timeout: 8_000 }); // Type a query and submit the search form await search_input.fill('Scott'); await page.locator('button:has-text("Search")').click(); // Result card with the badge full_name should appear await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 }); // The "Add" button must be visible (allow_tracking=true on the mock badge) await expect( page.locator('.results-list button.preset-filled-success') ).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 3. Opted-out attendee shows "Opt-Out" badge instead of Add button // ----------------------------------------------------------------------- test('opted-out attendee shows Opt-Out label instead of Add button', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, leads_overrides: search_mode_seed, // Override badge_li with one badge that has allow_tracking=false badge_li: [minimal_badge({ allow_tracking: false, full_name: 'Jane Opted Out' })], }); await page.goto(exhibit_url); await page.locator('header button.preset-filled-primary').click(); const search_input = page.locator( 'input[placeholder="Attendee name, email, or badge ID..."]' ); await expect(search_input).toBeVisible({ timeout: 8_000 }); await search_input.fill('Jane'); await page.locator('button:has-text("Search")').click(); await expect(page.locator('text=Jane Opted Out')).toBeVisible({ timeout: 8_000 }); // Add button must NOT appear await expect( page.locator('.results-list button.preset-filled-success') ).not.toBeVisible(); // "Opt-Out" label must appear instead await expect(page.locator('text=Opt-Out')).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 4. Clicking Add creates a lead and shows View link // // After a successful create_ae_obj__exhibit_tracking(), the search result // row switches from "Add" button to a "View" link pointing to the lead // detail page. This is the primary success path. // ----------------------------------------------------------------------- test('clicking Add button creates lead and shows View link', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, leads_overrides: search_mode_seed, }); await page.goto(exhibit_url); await page.locator('header button.preset-filled-primary').click(); const search_input = page.locator( 'input[placeholder="Attendee name, email, or badge ID..."]' ); await expect(search_input).toBeVisible({ timeout: 8_000 }); await search_input.fill('Scott'); await page.locator('button:has-text("Search")').click(); // Wait for results await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 }); // Intercept the tracking create request before clicking Add const create_promise = page.waitForRequest( (r) => r.url().includes('event_exhibit_tracking') && r.method() === 'POST', { timeout: 5_000 } ); await page.locator('.results-list button.preset-filled-success').click(); // The POST must have been made const create_req = await create_promise; const body = JSON.parse(create_req.postData() ?? '{}'); expect(body.event_badge_id).toBe('UIJT-73-63-61'); // Shared-passcode users store 'shared_passcode' as their identity expect(body.external_person_id).toBe('shared_passcode'); // "Add" button should disappear; "View" link should appear await expect( page.locator('.results-list button.preset-filled-success') ).not.toBeVisible({ timeout: 5_000 }); await expect( page.locator('.results-list a.preset-filled-secondary') ).toBeVisible({ timeout: 5_000 }); // View link must point to the tracking detail page const view_href = await page .locator('.results-list a.preset-filled-secondary') .getAttribute('href'); expect(view_href).toContain(`/leads/exhibit/${exhibit_id}/lead/`); }); // ----------------------------------------------------------------------- // 5. Search with no results shows empty-state message // ----------------------------------------------------------------------- test('search with no results shows empty-state message', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, leads_overrides: search_mode_seed, badge_li: [], // empty results }); await page.goto(exhibit_url); await page.locator('header button.preset-filled-primary').click(); const search_input = page.locator( 'input[placeholder="Attendee name, email, or badge ID..."]' ); await expect(search_input).toBeVisible({ timeout: 8_000 }); await search_input.fill('nobody'); await page.locator('button:has-text("Search")').click(); await expect( page.locator('text=No attendees found matching') ).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 6. "My Leads" filter resolves correctly for shared-passcode users // // When tracking__qry__licensee_email = 'my' and the user is authenticated // via shared passcode, the filter must resolve to 'shared_passcode' // (not the literal passcode string, which would never match any record). // This is the bug fixed in 2026-04-01 — a regression guard. // ----------------------------------------------------------------------- test('My Leads filter resolves to shared_passcode for shared-auth users', async ({ page }) => { const tracking_record = { id: 'TRK-MY-001', event_exhibit_tracking_id: 'TRK-MY-001', event_exhibit_id: exhibit_id, event_badge_id: 'UIJT-73-63-61', event_badge_full_name: 'Scott Idem', event_badge_email: 'scott@demo.oneskyit.com', // Stored identity for shared-passcode captures must be the literal // 'shared_passcode', not the actual passcode value. external_person_id: 'shared_passcode', group: 'shared_passcode', enable: 1, hide: false, created_on: new Date().toISOString(), updated_on: new Date().toISOString(), }; await setup_leads_test_page(page, event_id, exhibit_id, { auth_kv: signed_in_kv, tracking_li: [tracking_record], leads_overrides: { // Pre-set the filter to "My Leads" tracking__qry__licensee_email: 'my', }, }); await page.goto(exhibit_url); // The list tab should show; with the "My Leads" filter active and the // tracking record's external_person_id = 'shared_passcode', it must pass // through the HARD GUARD in filtered_lead_li. // // We cannot directly assert the filter resolved correctly without reading // the store, but we can assert the lead card IS visible (meaning the filter // did not incorrectly drop it). If the filter resolved to the raw passcode // string ('BOOTH2026'), the record would be excluded and the list empty. await expect(page.locator('text=Lead List')).toBeVisible({ timeout: 8_000 }); // The search will run against Dexie (which is empty — IDB not pre-seeded here). // Asserting no crash and correct page structure is the smoke-level check. // Full IDB-backed "My Leads" verification belongs in an IDB inject-then-reload test. await expect(page.locator('.ae_events_leads_tracking_new')).toBeVisible(); }); });