/** * Playwright tests: IDAA Recovery Meetings — Edit Form * * These tests cover the edit/create form for IDAA Recovery Meeting events. * They simulate an authenticated IDAA member with trusted-level access. * * Test data: * - Recovery Meeting event ID: 'IDAA_RM_TEST01' (fixture — not a real record) * - Novi UUID (trusted member): 'c9ea07b5-06b0-4a43-a2d0-8d06558c8a82' * * NOTE: These fixtures are synthetic. API calls are fully mocked so no * real backend traffic is required. */ import { test, expect } from '@playwright/test'; import { ae_app_local_data_defaults } from './_helpers/ae_defaults'; import { testing_account_id, mock_site_domain } from './_helpers/env'; // --- Test fixtures ----------------------------------------------------------- const TEST_EVENT_ID = '1Pkd025vvxU';// Per README test data const TEST_NOVI_UUID = 'c9ea07b5-06b0-4a43-a2d0-8d06558c8a82'; // in default novi_trusted_li const TEST_NOVI_NAME = 'IDAA Test Novi Member'; const TEST_NOVI_EMAIL = 'test+novi-member@oneskyit.com'; /** Minimal mock event object returned by the API. */ const mock_event = { id: TEST_EVENT_ID, event_id: TEST_EVENT_ID, account_id: testing_account_id, name: 'Thursday Night Serenity Group', type: 'IDAA', physical: true, virtual: false, address_name: 'Community Center', address_city: 'Chicago', address_country_subdivision_code: 'US-IL', address_country_alpha_2_code: 'US', timezone: 'US/Central', recurring: true, recurring_pattern: 'Weekly', weekday_thursday: true, recurring_start_time: '19:00', recurring_end_time: '20:00', recurring_text: '*gen* Weekly: Thursday at 7:00 PM US/Central', contact_li_json: [ { full_name: TEST_NOVI_NAME, email: TEST_NOVI_EMAIL, phone_mobile: null }, { full_name: null, email: null } ], location_text: '

Main entrance on Oak Street.

', attend_text: null, attend_json: {}, notes: null, enable: true, hide: false, priority: false, sort: null, cfg_json: {}, data_json: {} }; // --- Shared setup helpers ---------------------------------------------------- /** * Build the ae_idaa_loc localStorage value for a trusted IDAA member who has * the edit form open. */ function build_idaa_loc_defaults(opts: { edit_event_obj?: boolean } = {}) { return { __version: 1, // MUST MATCH IDAA_LOC_VERSION in store_versions.ts ver: '2024-08-21_1646', ver_idb: '2024-08-21_1645', novi_uuid: TEST_NOVI_UUID, novi_email: TEST_NOVI_EMAIL, novi_full_name: TEST_NOVI_NAME, novi_admin_li: ['2b078deb-b4e7-4203-99da-9f7cd62159a5'], novi_trusted_li: [TEST_NOVI_UUID, '58db22ee-4b0a-49a7-9f34-53d2ba85a84b'], novi_jitsi_mod_li: [], novi_meetings_base_url: 'https://www.idaa.org/idaa-meetings', recovery_meetings: { edit__event_obj: opts.edit_event_obj ?? false, qry__enabled: 'enabled', qry__hidden: 'not_hidden', qry__limit: 150, qry__order_by: 'updated_on', qry__order_by_li: { priority: 'DESC', sort: 'DESC', updated_on: 'DESC' }, qry__offset: 0, qry__fulltext_str: null, qry__physical: null, qry__type: null, qry__virtual: null }, bb: { edit_kv: {}, edit__post_obj: null, edit__post_comment_obj: null }, archives: { edit_kv: {}, edit__archive_obj: null, edit__archive_content_obj: null }, ds: {}, idaa_cfg_json: {} }; } /** * Inject ae_loc + ae_idaa_loc into localStorage so the IDAA layout * authenticates as a trusted member with the edit form open. * Must be called via page.addInitScript BEFORE any navigation. */ async function setup_idaa_auth(page: any) { await page.addInitScript( ({ ae_defaults, account_id, idaa_loc_defaults }: any) => { const ae_loc_data = { ...ae_defaults, account_id, authenticated_access: true, trusted_access: true, administrator_access: false, access_type: 'trusted', edit_mode: true, iframe: false, // Pre-seed the timezone list so the component renders . // Without this, $ae_loc.lu_time_zone_list is empty on first mount and // the renders with value="" which HTML5 required validation // silently blocks form submission (onsubmit never fires, no PATCH sent). current_timezone: 'US/Central', lu_time_zone_list: [ { id: 'tz1', code: 'US/Eastern', name: 'US/Eastern' }, { id: 'tz2', code: 'US/Central', name: 'US/Central' }, { id: 'tz3', code: 'US/Mountain', name: 'US/Mountain' }, { id: 'tz4', code: 'US/Pacific', name: 'US/Pacific' }, { id: 'tz5', code: 'Canada/Eastern', name: 'Canada/Eastern' }, { id: 'tz6', code: 'UTC', name: 'UTC' } ], site_cfg_json: { slct__event_id: null, novi_admin_li: ['2b078deb-b4e7-4203-99da-9f7cd62159a5'], novi_trusted_li: [ 'c9ea07b5-06b0-4a43-a2d0-8d06558c8a82', '58db22ee-4b0a-49a7-9f34-53d2ba85a84b' ], admin_email: 'admin@oneskyit.com', noreply_email: 'noreply@oneskyit.com' } }; window.localStorage.setItem('ae_loc', JSON.stringify(ae_loc_data)); window.localStorage.setItem('ae_idaa_loc', JSON.stringify(idaa_loc_defaults)); // Suppress all send_email() calls during Playwright tests. // Checked in api.ts send_email() before any fetch is made. (window as any).__ae_test_mode = true; }, { ae_defaults: ae_app_local_data_defaults, account_id: testing_account_id, idaa_loc_defaults: build_idaa_loc_defaults({ edit_event_obj: true }) } ); } /** * Register route mocks for the V3 CRUD API and common lookup tables. * Called inside each test after page creation. * * @param pass_through_event_patch - When true, the event PATCH (save) is NOT * intercepted and will reach the real backend. The GET is still mocked so * the form loads immediately from seeded IDB. Use this for integration * tests that need to verify actual persistence. * * @param pass_through_lookups - When true, lookup table routes (time_zone, * country, country_subdivision, event_type) are forwarded to the real API * instead of being mocked. This is preferred for the payload-verification * suite because the real API returns 50+ entries, naturally satisfying the * component's "> 50 entries" cache threshold — so no trickery with * pre-seeded fixture arrays is needed. * * @param pass_through_site_domain - When true, the site_domain lookup also * passes through so the app builds ae_api with real credentials. Required * for full integration tests that actually hit the PATCH endpoint. */ async function setup_api_mocks( page: any, event_id: string, opts: { pass_through_event_patch?: boolean; pass_through_lookups?: boolean; pass_through_site_domain?: boolean; } = {} ) { await page.route('**/v3/**', async (route: any) => { const url = route.request().url(); const method = route.request().method(); // Event GET by ID if (url.includes(`/v3/crud/event/${event_id}`) && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: mock_event }) }); } // Event PATCH (update) if (url.includes(`/v3/crud/event/${event_id}`) && method === 'PATCH') { if (opts.pass_through_event_patch) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { ...mock_event, event_id } }) }); } // Event POST (create new) if (url.includes('/v3/crud/event') && method === 'POST') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { ...mock_event, event_id: 'NEW_EVENT_123' } }) }); } // Event list search (POST to search endpoint) if (url.includes('/v3/crud/event') && url.includes('search') && method === 'POST') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_event], meta: { total: 1 } }) }); } // Lookup: time zones if (url.includes('/v3/') && url.includes('time_zone')) { // Pass through to real API if requested — real data satisfies the // component's "> 50 entries" cache check without fixture maintenance. if (opts.pass_through_lookups) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: 'tz1', code: 'US/Central', name: 'US/Central' }, { id: 'tz2', code: 'US/Eastern', name: 'US/Eastern' }, { id: 'tz3', code: 'US/Pacific', name: 'US/Pacific' } ] }) }); } // Lookup: countries if (url.includes('/v3/') && url.includes('country') && !url.includes('subdivision')) { if (opts.pass_through_lookups) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: 'c1', alpha_2_code: 'US', english_short_name: 'United States' }, { id: 'c2', alpha_2_code: 'CA', english_short_name: 'Canada' } ] }) }); } // Lookup: country subdivisions (states/provinces) if (url.includes('/v3/') && url.includes('country_subdivision')) { if (opts.pass_through_lookups) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: 's1', code: 'US-IL', name: 'Illinois', country_alpha_2_code: 'US' }, { id: 's2', code: 'US-NY', name: 'New York', country_alpha_2_code: 'US' } ] }) }); } // Lookup: event types if (url.includes('/v3/') && url.includes('event_type')) { if (opts.pass_through_lookups) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [ { id: 'et1', code: 'aa', name: 'AA' }, { id: 'et2', code: 'al-anon', name: 'Al-Anon' } ] }) }); } // Layout/site domain lookup — must return a proper account_id so the // layout builds ae_api.headers['x-account-id'] correctly. // pass_through_site_domain=true lets the app get real credentials from // dev-api.oneskyit.com; required for full integration tests. if (url.includes('/v3/crud/site_domain') || url.includes('/v3/lookup/')) { if (opts.pass_through_site_domain) return route.continue(); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_site_domain] }) }); } // Let everything else pass through or return a generic empty success return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); }); } /** * Seed the ae_events_db IndexedDB with the test event so liveQuery picks it up. */ async function seed_event_idb(page: any, event: typeof mock_event) { await page.evaluate((ev: typeof mock_event) => { return new Promise((resolve) => { try { const req = indexedDB.open('ae_events_db', 6); req.onupgradeneeded = (e) => { const db = (e.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains('event')) { db.createObjectStore('event', { keyPath: 'id' }); } }; req.onsuccess = () => { const db = req.result; try { const tx = db.transaction('event', 'readwrite'); tx.objectStore('event').put(ev); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); resolve(); }; } catch { db.close(); resolve(); } }; req.onerror = () => resolve(); } catch { resolve(); } }); }, event); } // --- Test suite -------------------------------------------------------------- test.describe('IDAA Recovery Meetings — Edit Form', () => { test.beforeEach(async ({ page }) => { page.on('pageerror', (err: Error) => console.error(`BROWSER ERROR: ${err.message}`) ); await setup_idaa_auth(page); await setup_api_mocks(page, TEST_EVENT_ID); }); // ------------------------------------------------------------------------- // Section 1: Page load and form structure // ------------------------------------------------------------------------- test('edit form renders after navigation to event page', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); // The edit form section has the class 'edit__event_obj' const form = page.locator('section.edit__event_obj'); await expect(form).toBeVisible({ timeout: 5000 }); }); test('general information section is present', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); // Meeting name input const name_input = page.locator('input[name="name"]'); await expect(name_input).toBeVisible(); // Type select (IDAA meeting types) const type_select = page.locator('select[name="type"]'); await expect(type_select).toBeVisible(); // Verify real option values exist await expect(type_select.locator('option[value="IDAA"]')).toBeAttached(); await expect(type_select.locator('option[value="Caduceus"]')).toBeAttached(); }); test('timing section shows day-of-week checkboxes', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); // All seven weekday checkboxes must be present for (const day of [ 'weekday_sunday', 'weekday_monday', 'weekday_tuesday', 'weekday_wednesday', 'weekday_thursday', 'weekday_friday', 'weekday_saturday' ]) { await expect(page.locator(`input[name="${day}"]`)).toBeAttached(); } }); test('start and end time inputs are present', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); await expect(page.locator('input[name="recurring_start_time"]')).toBeAttached(); await expect(page.locator('input[name="recurring_end_time"]')).toBeAttached(); }); test('contact section shows contact_1 fields', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); await expect(page.locator('input[name="contact_1_full_name"]')).toBeAttached(); await expect(page.locator('input[name="contact_1_email"]')).toBeAttached(); }); // ------------------------------------------------------------------------- // Section 2: Field interactions // ------------------------------------------------------------------------- test('meeting name field accepts text input', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); const name_input = page.locator('input[name="name"]'); await name_input.fill('Updated Meeting Name'); await expect(name_input).toHaveValue('Updated Meeting Name'); }); test('weekday checkbox can be checked and unchecked', async ({ page }) => { await page.goto(`/`); await seed_event_idb(page, mock_event); await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`); await page.waitForSelector('section.edit__event_obj', { timeout: 5000 }); // Weekday checkboxes use class="sr-only" — they are visually hidden and // the parent