import { test, expect } from '@playwright/test'; import { testing_event_id } from './_helpers/env'; import { testing_exhibit_id, testing_exhibit_id_b, exhibit_staff_passcode, setup_leads_test_page, attach_leads_routes, seed_ae_loc, seed_leads_loc, seed_events_loc, minimal_exhibit, } 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}`; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Locator for the sign-in form card. */ const signin_form = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator('.exhibit-signin'); /** Locator for the shared-passcode input field. */ const passcode_input = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator( 'input[placeholder="Enter shared code..."]' ); /** Locator for the "Add Lead" / "Lead List" toggle button — only visible when signed in. */ const header_action_btn = (page: Parameters[0]) => (page as import('@playwright/test').Page).locator( 'header button.preset-filled-primary' ); // --------------------------------------------------------------------------- // Test suite // --------------------------------------------------------------------------- test.describe('Leads — Auth Gate', () => { // ----------------------------------------------------------------------- // 1. Unauthenticated user sees sign-in form // ----------------------------------------------------------------------- test('unauthenticated user sees sign-in form, not the lead list', async ({ page }) => { // No auth_kv → no exhibit in auth_exhibit_kv → is_signed_in = false await setup_leads_test_page(page, event_id, exhibit_id); await page.goto(exhibit_url); // Sign-in form must appear within a reasonable time await expect(signin_form(page)).toBeVisible({ timeout: 10_000 }); // Header action buttons (Add Lead / Lead List) must NOT be visible await expect(header_action_btn(page)).not.toBeVisible(); }); // ----------------------------------------------------------------------- // 2. manager_access bypasses sign-in entirely // ----------------------------------------------------------------------- test('manager_access bypasses sign-in — list tab shown directly', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { // manager_access=true → is_signed_in = true without any exhibit auth access: { allow_access: true, authenticated_access: true, trusted_access: true, manager_access: true, }, }); await page.goto(exhibit_url); // Sign-in form must NOT appear await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 }); // Header action button (Add Lead / Lead List) must be visible await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 }); }); // ----------------------------------------------------------------------- // 3. Pre-authenticated user skips sign-in (already in auth_exhibit_kv) // ----------------------------------------------------------------------- test('already-signed-in user sees lead list, not sign-in form', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { // Seed auth_exhibit_kv — simulates a returning user whose session survived auth_kv: { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' }, }, }); 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 }); }); // ----------------------------------------------------------------------- // 4. Shared passcode sign-in — success path // // trusted_access = true causes the sign-in component to auto-fill the // passcode field once $lq__exhibit_obj loads into Dexie via the mocked API. // Waiting for the input to have a value gives us a reliable timing signal // (exhibit is in Dexie, $lq__exhibit_obj is live, sign-in will process). // ----------------------------------------------------------------------- test('shared passcode sign-in — correct passcode signs in successfully', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { // trusted_access triggers auto-fill once exhibit loads; no manager bypass access: { allow_access: true, trusted_access: true }, }); await page.goto(exhibit_url); // Sign-in form visible (trusted_access does NOT bypass the auth gate) await expect(signin_form(page)).toBeVisible({ timeout: 10_000 }); // Wait for exhibit to load into Dexie — auto-fill kicks in for trusted users. // Input having the correct value means $lq__exhibit_obj is live. await expect(passcode_input(page)).toHaveValue(exhibit_staff_passcode, { timeout: 10_000, }); // Submit await page.locator('button[type="submit"]').click(); // After 800 ms UX delay + Svelte reactivity, form should disappear await expect(signin_form(page)).not.toBeVisible({ timeout: 5_000 }); await expect(header_action_btn(page)).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 5. Wrong passcode shows error and keeps the form open // ----------------------------------------------------------------------- test('wrong passcode shows error message, form stays visible', async ({ page }) => { await setup_leads_test_page(page, event_id, exhibit_id, { access: { allow_access: true, trusted_access: true }, }); await page.goto(exhibit_url); // Wait for exhibit to be ready (auto-fill is the readiness signal) await expect(passcode_input(page)).toHaveValue(exhibit_staff_passcode, { timeout: 10_000, }); // Override with an incorrect passcode await passcode_input(page).fill('WRONGCODE'); await page.locator('button[type="submit"]').click(); // Error text must appear await expect( page.locator('text=Invalid shared passcode') ).toBeVisible({ timeout: 5_000 }); // Sign-in form must still be visible await expect(signin_form(page)).toBeVisible({ timeout: 3_000 }); // Header action button must NOT appear (not signed in) await expect(header_action_btn(page)).not.toBeVisible(); }); // ----------------------------------------------------------------------- // 6. Simultaneous multi-exhibit auth // // auth_exhibit_kv is keyed by exhibit_id, so Booth A and Booth B auth are // completely independent entries. A user can be signed into both at the // same time — this mirrors real-world use where staff manages adjacent booths. // // We pre-seed both exhibits in auth_exhibit_kv and verify that navigating // between them never triggers the sign-in form. // ----------------------------------------------------------------------- test('multi-exhibit: both booths authorized simultaneously, neither shows sign-in', async ({ page }) => { const exhibit_id_b = testing_exhibit_id_b; const exhibit_url_b = `/events/${event_id}/leads/exhibit/${exhibit_id_b}`; page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); // Route mock covering both exhibit IDs await page.route('**/v3/**', async (route) => { const req = route.request(); const url = req.url(); const method = req.method(); if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: '_6jcTbnJk-o', site_id: '92vkYC4fVEl', site_domain_id: '_6jcTbnJk-o', account_id: '_XY7DXtc9MY', account_id_random: '_XY7DXtc9MY', account_code: 'OSIT_DEMO', account_name: 'One Sky IT Demo', fqdn: 'demo.localhost:5173', enable: '1', cfg_json: {}, style_href: '', header_image_path: '', } ] }), }); } 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)) }); } if (url.includes(`/v3/crud/event_exhibit/${exhibit_id_b}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(minimal_exhibit(exhibit_id_b, { name: 'Test Booth — Beta Corp', code: 'BETA', staff_passcode: 'BOOTHB99', })) }); } 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, name: 'Test Event', mod_exhibits_json: { leads_require_payment: false } } }) }); } return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); }); // Seed ae_loc (regular shared-passcode user — no manager bypass) await seed_ae_loc(page, { allow_access: true }); // Pre-seed BOTH exhibits as authorized — independent KV entries. // Must seed ae_leads_loc (new PersistedState store) in addition to ae_events_loc. await seed_leads_loc(page, { auth_exhibit_kv: { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared', updated_on: new Date().toISOString() }, [exhibit_id_b]: { key: 'BOOTHB99', type: 'shared', updated_on: new Date().toISOString() }, }, }); await seed_events_loc(page, { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' }, [exhibit_id_b]: { key: 'BOOTHB99', type: 'shared' }, }); // Booth A → no sign-in form 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 }); // Booth B → no sign-in form (independent KV entry, unaffected by Booth A) await page.goto(exhibit_url_b); await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 }); await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 }); // Back to Booth A → still authorized 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 }); }); // ----------------------------------------------------------------------- // 7. Unauthorized exhibit shows sign-in even when another booth is auth'd // // Verifies that auth_exhibit_kv[booth_A] does not bleed into booth_B. // ----------------------------------------------------------------------- test('multi-exhibit: authorized for Booth A only → Booth B still shows sign-in', async ({ page }) => { const exhibit_id_b = testing_exhibit_id_b; const exhibit_url_b = `/events/${event_id}/leads/exhibit/${exhibit_id_b}`; page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); await page.route('**/v3/**', async (route) => { const url = route.request().url(); const method = route.request().method(); if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: '_6jcTbnJk-o', site_id: '92vkYC4fVEl', site_domain_id: '_6jcTbnJk-o', account_id: '_XY7DXtc9MY', account_id_random: '_XY7DXtc9MY', account_code: 'OSIT_DEMO', account_name: 'One Sky IT Demo', fqdn: 'demo.localhost:5173', enable: '1', cfg_json: {}, style_href: '', header_image_path: '' } ] }) }); } 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)) }); } if (url.includes(`/v3/crud/event_exhibit/${exhibit_id_b}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(minimal_exhibit(exhibit_id_b, { name: 'Beta Corp', code: 'BETA' })) }); } return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); }); await seed_ae_loc(page, { allow_access: true }); // Only Booth A authorized — Booth B not in auth_kv await seed_events_loc(page, { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' }, }); // Booth A → no sign-in form await page.goto(exhibit_url); await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 }); // Booth B → must show sign-in form (auth does not cross exhibit boundaries) await page.goto(exhibit_url_b); await expect(signin_form(page)).toBeVisible({ timeout: 10_000 }); await expect(header_action_btn(page)).not.toBeVisible(); }); });