import { test, expect } from '@playwright/test'; import { testing_event_id, mock_site_domain } from './_helpers/env'; import { minimal_event_for_leads, seed_ae_loc, } from './_helpers/leads_helpers'; const event_id = testing_event_id; const config_url = `/events/${event_id}/leads/config`; /** * Minimal route mock for the Leads Config page. * * The config page only needs the event record (not exhibit or badge data) plus * the site_domain init call. A PATCH handler is included for save tests. * * @param patch_handler Optional callback invoked when a PATCH to the event is intercepted. * Defaults to returning 200 OK. */ async function setup_config_routes( page: import('@playwright/test').Page, opts: { event_data_overrides?: Record; on_patch?: (body: any) => void; } = {} ) { const { event_data_overrides = {}, on_patch } = opts; await page.route('**/v3/**', async (route) => { const req = route.request(); const url = req.url(); const method = req.method(); // Site domain init if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_site_domain] }), }); } // Event GET — provides mod_exhibits_json for the draft 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)), }); } // Event PATCH — save config if (url.includes(`/v3/crud/event/${event_id}`) && method === 'PATCH') { const raw = await req.postData(); const body = raw ? JSON.parse(raw) : {}; on_patch?.(body); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: event_id } }), }); } return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }), }); }); } test.describe('Leads — Config Page', () => { // ----------------------------------------------------------------------- // 1. Non-admin user sees access-denied screen // // The config page is administrator_access only. Any other access level must // see the Lock icon and "Administrator access required." — no form fields. // ----------------------------------------------------------------------- test('non-admin user sees access-denied message, no form', async ({ page }) => { page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); await setup_config_routes(page); await seed_ae_loc(page, { allow_access: true, trusted_access: true }); await page.goto(config_url); // Access denied block must be visible await expect( page.locator('text=Administrator access required.') ).toBeVisible({ timeout: 10_000 }); // Form / save button must NOT be visible await expect(page.locator('text=Leads Config')).not.toBeVisible(); await expect(page.locator('button:has-text("Save")')).not.toBeVisible(); }); // ----------------------------------------------------------------------- // 2. Admin user sees config form // // administrator_access = true reveals the full form with both sections // (Payment and Stripe Keys) and the Save button. // ----------------------------------------------------------------------- test('admin user sees config form with Payment and Stripe sections', async ({ page }) => { page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); await setup_config_routes(page); await seed_ae_loc(page, { allow_access: true, authenticated_access: true, trusted_access: true, administrator_access: true, }); await page.goto(config_url); // Heading confirms page rendered await expect(page.locator('h1:has-text("Leads Config")')).toBeVisible({ timeout: 10_000 }); // Both collapsible sections are visible (default open) // Use the checkbox label text — it's unique inside the Payment section await expect(page.locator('text=Require Payment (Stripe)')).toBeVisible({ timeout: 5_000 }); await expect(page.locator('text=Stripe Keys')).toBeVisible({ timeout: 5_000 }); // Save button is present — two exist (header + bottom), check the header one await expect(page.locator('button:has-text("Save")').first()).toBeVisible({ timeout: 5_000 }); }); // ----------------------------------------------------------------------- // 3. Save button disabled until form is dirty // // On load, is_dirty = false because draft JSON === initial JSON. // After toggling the "Require Payment" checkbox, is_dirty = true and the // Save button becomes enabled. // ----------------------------------------------------------------------- test('save button disabled by default, enabled after checkbox change', async ({ page }) => { page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); // Start with leads_require_payment: false so toggling it makes a real change await setup_config_routes(page, { event_data_overrides: { mod_exhibits_json: { leads_require_payment: false }, }, }); await seed_ae_loc(page, { allow_access: true, authenticated_access: true, trusted_access: true, administrator_access: true, }); await page.goto(config_url); // Wait for event to load and draft to initialize await page.waitForResponse( (r) => r.url().includes(`crud/event/${event_id}`) && r.status() === 200, { timeout: 8_000 } ); // Wait for the form to render (draft_initialized) await expect( page.locator('input[type="checkbox"]').first() ).toBeVisible({ timeout: 8_000 }); // Save must be disabled (not dirty yet) await expect(page.locator('button:has-text("Save Config")')).toBeDisabled({ timeout: 3_000 }); // Toggle the "Require Payment" checkbox await page.locator('input[type="checkbox"]').first().click(); // Save must now be enabled await expect(page.locator('button:has-text("Save Config")')).toBeEnabled({ timeout: 3_000 }); }); // ----------------------------------------------------------------------- // 4. Save sends PATCH with mod_exhibits_json and shows "Saved" badge // // Clicking the Save button must PATCH event.mod_exhibits_json via the V3 // API. The "Saved" badge must appear confirming success. // ----------------------------------------------------------------------- test('save sends PATCH to event and shows Saved badge', async ({ page }) => { page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); let patched_body: any = null; await setup_config_routes(page, { event_data_overrides: { mod_exhibits_json: { leads_require_payment: false }, }, on_patch: (body) => { patched_body = body; }, }); await seed_ae_loc(page, { allow_access: true, authenticated_access: true, trusted_access: true, administrator_access: true, }); await page.goto(config_url); // Wait for form to initialize await page.waitForResponse( (r) => r.url().includes(`crud/event/${event_id}`) && r.status() === 200, { timeout: 8_000 } ); await expect( page.locator('input[type="checkbox"]').first() ).toBeVisible({ timeout: 8_000 }); // Make a change to unlock save await page.locator('input[type="checkbox"]').first().click(); // Intercept the PATCH before clicking save const patch_promise = page.waitForRequest( (r) => r.url().includes(`crud/event/${event_id}`) && r.method() === 'PATCH', { timeout: 5_000 } ); await page.locator('button:has-text("Save Config")').click(); // PATCH must have been made await patch_promise; // "Saved" badge must appear await expect(page.locator('text=Saved')).toBeVisible({ timeout: 5_000 }); // The patched body must include mod_exhibits_json. // update_ae_obj auto-serializes *_json fields to strings before the PATCH, // so parse it back to an object before asserting field values. expect(patched_body?.mod_exhibits_json).toBeDefined(); const patched_cfg = typeof patched_body.mod_exhibits_json === 'string' ? JSON.parse(patched_body.mod_exhibits_json) : patched_body.mod_exhibits_json; expect(patched_cfg?.leads_require_payment).toBe(true); }); });