/** * tests/_helpers/leads_helpers.ts * * Playwright test helpers for the Exhibitor Leads module. * * Provides: * - Demo ID constants * - Data factories (exhibit, tracking, event-with-leads) * - localStorage seeders (ae_loc, ae_events_loc) * - V3 API route mocks for the exhibit page * - One-call beforeEach helper: setup_leads_test_page() * * Auth model: * ae_loc.manager_access = true → bypasses sign-in entirely (admin shortcut) * ae_events_loc.leads.auth_exhibit_kv[exhibit_id] → exhibit-level auth * { key: passcode_or_email, type: 'shared' | 'licensed', updated_on } * * Multiple exhibits can be authorized simultaneously — auth_exhibit_kv is keyed * by exhibit_id, so signing into booth A and booth B are independent entries. * * API routes mocked (all matching pattern `v3/...`): * GET /v3/crud/event_exhibit/{exhibit_id} → exhibit data (loads into Dexie) * GET /v3/crud/event/{event_id} → event data (mod_exhibits_json) * POST site_domain/search → mock site domain * POST exhibit_tracking/search → tracking list (empty by default) * any everything else → { data: [] } */ import type { Page } from '@playwright/test'; import { mock_site_domain, testing_account_id, testing_event_id } from './env'; import { ae_app_local_data_defaults } from './ae_defaults'; // --------------------------------------------------------------------------- // Demo IDs — real demo-DB rows under event 'pjrcghqwert' (1) "Demo One Sky IT Conference" // IDs are documented in tests/README.md under "Events Modules". // --------------------------------------------------------------------------- /** Primary demo exhibit — (1) "One Sky's Awesome Exhibit". */ export const testing_exhibit_id = 'xK_9yEj1bQY'; /** Secondary demo exhibit — (3) "Exhibit for Precon Events". Used in multi-exhibit auth tests. */ export const testing_exhibit_id_b = 'acHCkrCDaYs'; /** Shared staff passcode used in mocked API responses (not a real DB value). */ export const exhibit_staff_passcode = 'BOOTH2026'; // --------------------------------------------------------------------------- // Data factories // --------------------------------------------------------------------------- /** Minimal event object with mod_exhibits_json set for Leads. */ export const minimal_event_for_leads = ( event_id: string, overrides: Record = {} ) => ({ data: { id: event_id, event_id: event_id, name: 'Test Event (leads)', cfg_json: {}, mod_pres_mgmt_json: {}, mod_badges_json: {}, mod_abstracts_json: {}, mod_exhibits_json: { leads_require_payment: true, stripe_publishable_key: null, stripe_btn_1_license: null, stripe_btn_3_license: null, stripe_btn_6_license: null, stripe_btn_10_license: null, }, mod_meetings_json: {}, ...overrides, }, }); /** * Minimal exhibit object. * * Includes staff_passcode so the sign-in form can validate against it. * The API response is wrapped in { data: ... } to match V3 format. */ export const minimal_exhibit = ( exhibit_id: string, overrides: Record = {} ) => ({ data: { id: exhibit_id, event_exhibit_id: exhibit_id, event_id: testing_event_id, name: 'Test Booth — ACME Corp', code: 'ACME', staff_passcode: exhibit_staff_passcode, license_max: 3, license_li_json: '[]', leads_api_access: true, leads_custom_questions_json: '[]', leads_device_sm_qty: 0, leads_device_lg_qty: 0, enable: '1', hide: '0', priority: '0', sort: '0', group: null, notes: null, cfg_json: null, data_json: null, created_on: '2026-01-01T00:00:00', updated_on: '2026-01-01T00:00:00', ...overrides, }, }); /** * Minimal event badge for use in badge search results. * * allow_tracking MUST be true for the "Add" button to appear. * Uses the real demo badge ID from tests/README.md. */ export const minimal_badge = (overrides: Record = {}) => ({ event_badge_id: 'UIJT-73-63-61', // demo badge (37163) "Scott Idem" event_badge_id_random: 'UIJT-73-63-61', full_name: 'Scott Idem', email: 'scott@demo.oneskyit.com', affiliations: 'One Sky IT', allow_tracking: true, // required for Add button to render badge_type: 'attendee', ...overrides, }); /** Minimal exhibit tracking record (a captured lead). */ export const minimal_tracking = ( exhibit_id: string, tracking_id: string, overrides: Record = {} ) => ({ id: tracking_id, event_exhibit_tracking_id: tracking_id, event_exhibit_id: exhibit_id, event_badge_id: 'BADGE001', event_badge_full_name: 'Jane Doe', event_badge_email: 'jane@example.com', external_person_id: 'shared_passcode', // shared booth staff capture identity exhibitor_notes: '', enable: 1, hide: false, created_on: new Date().toISOString(), updated_on: new Date().toISOString(), ...overrides, }); // --------------------------------------------------------------------------- // localStorage seeders // --------------------------------------------------------------------------- /** * Seed ae_loc with the correct __version (2 = AE_LOC_VERSION) and optional * access flag overrides. * * Must be called via addInitScript so it runs before store_versions.ts wipes * stale data. If __version doesn't match AE_LOC_VERSION, the store is wiped * and auth state is lost — this helper ensures version is always correct. * * Common access overrides: * { manager_access: true } → bypasses Leads sign-in (admin shortcut) * { trusted_access: true } → triggers passcode auto-fill in sign-in form */ export async function seed_ae_loc( page: Page, overrides: Record = {} ): Promise { await page.addInitScript( ([defaults, ovrd]: [typeof ae_app_local_data_defaults, Record]) => { const data = { ...defaults, ...ovrd, __version: 2 }; window.localStorage.setItem('ae_loc', JSON.stringify(data)); }, [ae_app_local_data_defaults, overrides] as [ typeof ae_app_local_data_defaults, Record ] ); } /** * Seed ae_leads_loc (new Svelte-5 PersistedState store) with defaults and __version. * Must be called via addInitScript so it runs before store_versions.ts wipes stale data. */ export async function seed_leads_loc( page: Page, overrides: Record = {} ): Promise { await page.addInitScript(([ovrd]: [Record]) => { const defaults = { __version: 1, show_option__paid_tab: true, show_content__scan_alert: true, show_content__scan_requirements: true, show_content__custom_question_descriptions: true, show_content__email_link_warning: true, default_to_scan: true, default__external_registration_id: '2024_Annual Meeting', auto_view: true, auto_hide_on_sign_in: true, show_hidden: false, show_not_enabled: false, refresh_interval__tracking_li: 30000, refresh_interval_sec: 25, search_version: 0, qry__remote_first: false, qry__search_text: '', qry__sort_order: 'name_asc', tracking__search_version: 0, tracking__qry__remote_first: false, tracking__qry__search_text: '', tracking__qry__sort_order: 'created_desc', tracking__qry__licensee_email: 'all', entered_passcode: null, auth_exhibit_kv: {}, edit_license_li: false, tab: {}, tab_add_mode: {}, tab_scan_qualify: {} }; const data = { ...defaults, ...ovrd, __version: 1 }; window.localStorage.setItem('ae_leads_loc', JSON.stringify(data)); }, [overrides] as [Record]); } /** * Seed ae_events_loc with the correct __version (1 = AE_EVENTS_LOC_VERSION) * and the given per-exhibit auth entries. * * auth_kv shape: { [exhibit_id]: { key: string; type: 'shared' | 'licensed' } } * * Multiple exhibit IDs can be passed simultaneously — each becomes an * independent entry in auth_exhibit_kv, so a user can be signed into * Booth A and Booth B at the same time (separate auth scopes). * * An empty auth_kv (default) means no exhibits are authorized → sign-in * form is shown. * * @param leads_overrides Extra fields merged into the `leads` namespace, e.g. * `{ tab_add_mode: { [exhibit_id]: 'search' } }` to start in manual search mode. */ export async function seed_events_loc( page: Page, auth_kv: Record = {}, leads_overrides: Record = {} ): Promise { await page.addInitScript( ([kv, lo]: [Record, Record]) => { // Build auth_exhibit_kv with timestamps added server-side const auth_exhibit_kv: Record< string, { key: string; type: string; updated_on: string } > = {}; for (const [eid, entry] of Object.entries(kv)) { auth_exhibit_kv[eid] = { ...entry, updated_on: new Date().toISOString() }; } const events_loc = { __version: 1, // Must match AE_EVENTS_LOC_VERSION in store_versions.ts // Deployment stamp — must match events_sess.ver to avoid "new version available" banner. ver: '2025-10-16_2139', name: 'Aether - Events', title: "OSIT's Æ Events", ds: {}, events_cfg_json: {}, event_id: null, qry__enabled: 'enabled', qry__hidden: 'not_hidden', qry__limit: 20, qry__offset: 0, show_details: false, auth__person: {}, auth__kv: { event: {}, exhibit: {}, location: {}, session: {}, presentation: {}, presenter: {}, person: {}, }, // Non-leads modules: empty — tests only exercise leads routes badges: {}, launcher: {}, pres_mgmt: {}, leads: { // Leads loc defaults (inlined — avoids SvelteKit module resolution in Node test runner) show_option__paid_tab: true, show_content__scan_alert: true, show_content__scan_requirements: true, show_content__custom_question_descriptions: true, show_content__email_link_warning: true, default_to_scan: true, default__external_registration_id: '2024_Annual Meeting', auto_view: true, auto_hide_on_sign_in: true, show_hidden: false, show_not_enabled: false, refresh_interval__tracking_li: 30000, search_version: 0, qry__remote_first: false, qry__search_text: '', qry__sort_order: 'name_asc', tracking__search_version: 0, tracking__qry__remote_first: false, tracking__qry__search_text: '', tracking__qry__sort_order: 'created_desc', tracking__qry__licensee_email: 'all', entered_passcode: null, edit_license_li: false, tab: {}, tab_add_mode: {}, tab_scan_qualify: {}, // The auth entries for this test session auth_exhibit_kv, // Any extra leads fields (e.g. tab_add_mode, tab_scan_qualify) ...lo, }, }; window.localStorage.setItem('ae_events_loc', JSON.stringify(events_loc)); }, [auth_kv, leads_overrides] as [Record, Record]); } // --------------------------------------------------------------------------- // Route mocks // --------------------------------------------------------------------------- /** * Attach V3 API route mocks for the exhibit page to the given Playwright page. * * Intercepts all requests matching the `v3/` pattern and fulfills them with minimal mock data so * the app can initialize (site_domain → ae_loc hydration, event → event record, * event_exhibit → exhibit record written into Dexie for sign-in form). * * @param tracking_li Pre-existing leads to include in tracking/search responses. * @param badge_li Badges returned by event_badge/search. Defaults to one badge * with allow_tracking=true (the demo "Scott Idem" badge). * @param event_data_overrides Merged into the `data` object of the event response. * Use to override mod_exhibits_json, e.g. * `{ mod_exhibits_json: { leads_require_payment: false } }`. */ export async function attach_leads_routes( page: Page, event_id: string, exhibit_id: string, opts: { staff_passcode?: string; exhibit_name?: string; exhibit_overrides?: Record; tracking_li?: any[]; badge_li?: any[]; event_data_overrides?: Record; } = {} ): Promise { const { staff_passcode = exhibit_staff_passcode, exhibit_name = 'Test Booth — ACME Corp', exhibit_overrides = {} as Record, tracking_li = [], badge_li = [minimal_badge()], event_data_overrides = {}, } = opts; // In-memory state for newly created tracking records within this session let created_tracking: any = null; await page.route('**/v3/**', async (route) => { const req = route.request(); const url = req.url(); const method = req.method(); // Site domain init — required for ae_loc hydration in the root layout if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_site_domain] }), }); } // Exhibit record — loaded by +layout.ts into Dexie so the sign-in form // can check $lq__exhibit_obj.staff_passcode if (url.includes(`/v3/crud/event_exhibit/${exhibit_id}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify( minimal_exhibit(exhibit_id, { staff_passcode, name: exhibit_name, ...exhibit_overrides }) ), }); } // Event record — needed for mod_exhibits_json (leads_require_payment, Stripe config) if (url.includes(`/v3/crud/event/${event_id}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(minimal_event_for_leads(event_id, event_data_overrides)), }); } // Badge search — returns badge_li for any event_badge search // URL: POST /v3/crud/event/{event_id}/event_badge/search if (url.includes('event_badge') && url.includes('search') && method === 'POST') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: badge_li }), }); } // Tracking create — POST /v3/crud/event_exhibit/{exhibit_id}/event_exhibit_tracking/ // Returns a new tracking record; also adds it to the in-session list so subsequent // tracking searches include it. if ( url.includes(`event_exhibit/${exhibit_id}/event_exhibit_tracking`) && method === 'POST' ) { const post = await req.postData(); const body = post ? JSON.parse(post) : {}; created_tracking = { id: 'TRK-TEST-001', event_exhibit_tracking_id: 'TRK-TEST-001', event_exhibit_tracking_id_random: 'TRK-TEST-001', event_exhibit_id: exhibit_id, event_badge_id: body.event_badge_id ?? 'UIJT-73-63-61', event_badge_full_name: 'Scott Idem', event_badge_email: 'scott@demo.oneskyit.com', external_person_id: body.external_person_id ?? 'shared_passcode', group: body.group ?? 'shared_passcode', exhibitor_notes: '', enable: 1, hide: false, created_on: new Date().toISOString(), updated_on: new Date().toISOString(), }; return route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ data: created_tracking }), }); } // Tracking search — returns pre-populated list + any newly created record if (url.includes('exhibit_tracking') && url.includes('search') && method === 'POST') { const all = created_tracking ? [...tracking_li, created_tracking] : tracking_li; return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: all }), }); } // Fallback: return empty success for any other V3 request (tracking list, etc.) return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }), }); }); } // --------------------------------------------------------------------------- // One-call beforeEach // --------------------------------------------------------------------------- /** * Canonical beforeEach helper for leads exhibit page tests. * * Wires up in order (order matters — addInitScript runs in insertion order): * 1. page.on('pageerror') → stderr * 2. attach_leads_routes() — intercept all /v3/ API calls * 3. seed_ae_loc() — seed ae_loc (correct __version + access flags) * 4. seed_events_loc() — seed ae_events_loc (optional exhibit auth entries) * * @param access ae_loc overrides e.g. { manager_access: true } * @param auth_kv Per-exhibit auth — pass {} for no auth (shows sign-in form) * @param leads_overrides Extra fields merged into ae_events_loc.leads namespace, * e.g. { tab_add_mode: { [exhibit_id]: 'search' } } */ export async function setup_leads_test_page( page: Page, event_id: string, exhibit_id: string, opts: { access?: Record; auth_kv?: Record; leads_overrides?: Record; staff_passcode?: string; exhibit_name?: string; exhibit_overrides?: Record; tracking_li?: any[]; badge_li?: any[]; event_data_overrides?: Record; } = {} ): Promise { const { access = {}, auth_kv = {}, leads_overrides = {}, ...route_opts } = opts; // Build auth_exhibit_kv with timestamps for ae_leads_loc (the current store). // seed_events_loc also seeds the old ae_events_loc for backwards compatibility, // but is_signed_in now reads from leads_loc (ae_leads_loc) exclusively. const auth_exhibit_kv_with_ts: Record = {}; for (const [eid, entry] of Object.entries(auth_kv)) { auth_exhibit_kv_with_ts[eid] = { ...entry, updated_on: new Date().toISOString() }; } page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); await attach_leads_routes(page, event_id, exhibit_id, route_opts); await seed_ae_loc(page, access); await seed_leads_loc(page, { ...leads_overrides, auth_exhibit_kv: auth_exhibit_kv_with_ts }); await seed_events_loc(page, auth_kv, leads_overrides); } export default { testing_exhibit_id, testing_exhibit_id_b, exhibit_staff_passcode, minimal_event_for_leads, minimal_exhibit, minimal_badge, minimal_tracking, seed_ae_loc, seed_leads_loc, seed_events_loc, attach_leads_routes, setup_leads_test_page, };