import { test, expect } from '@playwright/test'; import { testing_event_id } from './_helpers/env'; import { testing_exhibit_id, exhibit_staff_passcode, setup_leads_test_page, } 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}`; // --------------------------------------------------------------------------- // Test license entries // --------------------------------------------------------------------------- /** A licensed staff member whose leads should be tagged with their email. */ const licensed_user = { full_name: 'Alice Staff', email: 'alice@acmecorp.com', passcode: 'ALICE99', }; /** A second licensed user — verifies identity isolation between staff members. */ const licensed_user_b = { full_name: 'Bob Staff', email: 'bob@acmecorp.com', passcode: 'BOB88', }; /** * license_li_json as a JSON string (matches how the API returns it and how * the sign-in component parses it inside handle_signin). */ const license_li_json = JSON.stringify([licensed_user, licensed_user_b]); // Exhibit overrides that inject the license list into the mocked API response. const exhibit_with_licenses = { license_li_json }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const signin_form = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator('.exhibit-signin'); const header_action_btn = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator( 'header button.preset-filled-primary' ); const passcode_input = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator( 'input[placeholder="Enter shared code..."]' ); const search_mode_seed = { tab_add_mode: { [exhibit_id]: 'search' } }; /** * Navigate to the exhibit URL and wait for lq__exhibit_obj to be live in the * sign-in component before returning. * * The sign-in component's handle_signin() returns early if lq__exhibit_obj is * null. trusted_access causes handle_signin to auto-fill the shared passcode * input once lq__exhibit_obj loads — this is the same readiness signal used * in the shared passcode sign-in tests. * * Note: trusted_access does NOT bypass the auth gate (only manager_access * does), so the sign-in form will still appear for licensed user tests. */ async function goto_and_wait_for_exhibit_ready( page: import('@playwright/test').Page ): Promise { await page.goto(exhibit_url); // Auto-fill fires once lq__exhibit_obj is live — this is our readiness signal. await expect(passcode_input(page)).toHaveValue(exhibit_staff_passcode, { timeout: 10_000, }); } // --------------------------------------------------------------------------- // Test suite // --------------------------------------------------------------------------- test.describe('Leads — Licensed User Sign-In', () => { // ----------------------------------------------------------------------- // 1. Licensed user tab is visible on the sign-in form // ----------------------------------------------------------------------- test('sign-in form has a Licensed User tab', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, }); await page.goto(exhibit_url); await expect(signin_form(page)).toBeVisible({ timeout: 10_000 }); await expect(page.locator('button:has-text("Licensed User")')).toBeVisible({ timeout: 5_000, }); }); // ----------------------------------------------------------------------- // 2. Correct email + passcode signs in successfully // ----------------------------------------------------------------------- test('correct email and passcode signs in — form disappears, lead list shown', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, }); await goto_and_wait_for_exhibit_ready(page); // Switch to Licensed User tab await page.locator('button:has-text("Licensed User")').click(); await page.locator('input[type="email"]').fill(licensed_user.email); await page.locator('input[placeholder="Your code..."]').fill(licensed_user.passcode); await page.locator('button[type="submit"]').click(); // After the 800 ms UX delay + reactivity, form should disappear await expect(signin_form(page)).not.toBeVisible({ timeout: 5_000 }); await expect(header_action_btn(page)).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 3. Wrong passcode shows error, form stays open // ----------------------------------------------------------------------- test('wrong passcode shows error message, form stays visible', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, }); await goto_and_wait_for_exhibit_ready(page); await page.locator('button:has-text("Licensed User")').click(); await page.locator('input[type="email"]').fill(licensed_user.email); await page.locator('input[placeholder="Your code..."]').fill('WRONGCODE'); await page.locator('button[type="submit"]').click(); await expect( page.locator('text=Invalid email or personal passcode') ).toBeVisible({ timeout: 5_000 }); await expect(signin_form(page)).toBeVisible({ timeout: 3_000 }); await expect(header_action_btn(page)).not.toBeVisible(); }); // ----------------------------------------------------------------------- // 4. Unknown email shows error, form stays open // ----------------------------------------------------------------------- test('unknown email shows error message, form stays visible', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, }); await goto_and_wait_for_exhibit_ready(page); await page.locator('button:has-text("Licensed User")').click(); await page.locator('input[type="email"]').fill('unknown@notregistered.com'); await page.locator('input[placeholder="Your code..."]').fill('ANYCODE'); await page.locator('button[type="submit"]').click(); await expect( page.locator('text=Invalid email or personal passcode') ).toBeVisible({ timeout: 5_000 }); await expect(signin_form(page)).toBeVisible({ timeout: 3_000 }); }); // ----------------------------------------------------------------------- // 5. Licensed user captures a lead — external_person_id = their email // // This is the core business rule: every lead captured by a licensed user // must be tagged with their email so "My Leads" filtering works per staff // member. The POST body is inspected directly to verify the identity field. // ----------------------------------------------------------------------- test('lead captured by licensed user is tagged with their email address', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, leads_overrides: search_mode_seed, }); await goto_and_wait_for_exhibit_ready(page); // Sign in as licensed user await page.locator('button:has-text("Licensed User")').click(); await page.locator('input[type="email"]').fill(licensed_user.email); await page.locator('input[placeholder="Your code..."]').fill(licensed_user.passcode); await page.locator('button[type="submit"]').click(); await expect(signin_form(page)).not.toBeVisible({ timeout: 5_000 }); // Navigate to Add Lead tab await header_action_btn(page).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(); await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 }); 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(); const create_req = await create_promise; const body = JSON.parse(create_req.postData() ?? '{}'); // The lead must be tagged with the licensed user's email, not 'shared_passcode' expect(body.external_person_id).toBe(licensed_user.email); expect(body.group).toBe(licensed_user.email); }); // ----------------------------------------------------------------------- // 6. Two different licensed users get different external_person_id values // // Regression guard: Bob's leads must be tagged with his email, not Alice's // and not 'shared_passcode'. // ----------------------------------------------------------------------- test('different licensed users are tagged with their own email addresses', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, access: { allow_access: true, trusted_access: true }, leads_overrides: search_mode_seed, }); await goto_and_wait_for_exhibit_ready(page); // Sign in as Bob (the second licensed user) await page.locator('button:has-text("Licensed User")').click(); await page.locator('input[type="email"]').fill(licensed_user_b.email); await page.locator('input[placeholder="Your code..."]').fill(licensed_user_b.passcode); await page.locator('button[type="submit"]').click(); await expect(signin_form(page)).not.toBeVisible({ timeout: 5_000 }); await header_action_btn(page).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(); await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 }); 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(); const create_req = await create_promise; const body = JSON.parse(create_req.postData() ?? '{}'); // Must be Bob's email, not Alice's and not 'shared_passcode' expect(body.external_person_id).toBe(licensed_user_b.email); expect(body.external_person_id).not.toBe(licensed_user.email); expect(body.external_person_id).not.toBe('shared_passcode'); }); // ----------------------------------------------------------------------- // 7. Pre-seeded licensed auth skips sign-in (returning user) // // A returning user whose session is already in auth_exhibit_kv with // type='licensed' should see the lead list directly — no sign-in form. // ----------------------------------------------------------------------- test('pre-seeded licensed auth skips sign-in form', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { exhibit_overrides: exhibit_with_licenses, auth_kv: { // Simulate a returning session: already signed in as Alice [exhibit_id]: { key: licensed_user.email, type: 'licensed' }, }, }); await page.goto(exhibit_url); await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 }); await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 }); }); });