diff --git a/tests/idaa_recovery_meeting_edit.test.ts b/tests/idaa_recovery_meeting_edit.test.ts index c0e0fd7f..b363d6b7 100644 --- a/tests/idaa_recovery_meeting_edit.test.ts +++ b/tests/idaa_recovery_meeting_edit.test.ts @@ -667,6 +667,11 @@ test.describe('IDAA Recovery Meetings — Real Backend Save (Integration)', () = // ============================================================================= test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () => { + // Each test calls open_edit_form which waits for real lookup API responses + // (pass_through_lookups=true). On slow network or under load those can take + // 20–25 s, leaving little margin at the default 30 s limit. 60 s gives a safe + // buffer without masking genuine hangs. + test.setTimeout(60000); test.beforeEach(async ({ page }) => { page.on('pageerror', (err: Error) => @@ -743,6 +748,44 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = }, { timeout: 5000 } ); + // Wait for the background SWR list fetch (load_ae_obj_li__event in +layout.ts) to + // have completed and written processed event data back to IDB. + // + // WHY THIS MATTERS: The layout fires load_ae_obj_li__event on every page load. + // It returns the IDB cache immediately (SWR), then fires a background API fetch. + // When the fetch completes, _process_generic_props processes the result and saves + // it to IDB — which triggers a liveQuery re-fire that resets reactive form inputs + // back to IDB values. If a test fills an input BEFORE this re-fire, the liveQuery + // overwrites it with the API value (e.g., address_name → undefined, attend_json → {}). + // + // _process_generic_props always computes tmp_sort_1 as part of processing. Its + // presence in IDB is the reliable signal that the background fetch has settled and + // no further liveQuery re-fires will overwrite values the test is about to fill. + await page.waitForFunction( + (event_id: string) => { + return new Promise((resolve) => { + try { + const req = indexedDB.open('ae_events_db', 6); + req.onerror = () => resolve(false); + req.onsuccess = (e: Event) => { + try { + const db = (e.target as IDBOpenDBRequest).result; + const tx = db.transaction(['event'], 'readonly'); + const store = tx.objectStore('event'); + const get_req = store.get(event_id); + get_req.onerror = () => resolve(false); + get_req.onsuccess = (e2: Event) => { + const rec = (e2.target as IDBRequest).result; + resolve(rec != null && rec.tmp_sort_1 != null); + }; + } catch { resolve(false); } + }; + } catch { resolve(false); } + }); + }, + TEST_EVENT_ID, + { timeout: 5000 } + ); } /** @@ -811,7 +854,10 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = const body = await capture_patch_body(page); - const addr = body.location_address_json; + // _json fields are auto-serialized to JSON strings by update_ae_obj_v3 before + // the PATCH is sent (Standard Aether Pattern: any key ending in _json is + // JSON.stringify'd so the backend receives a string, not an object). + const addr = JSON.parse(body.location_address_json); expect(addr, 'location_address_json is present').toBeTruthy(); expect(addr.name, 'address_name → .name').toBe('Test Community Hall'); expect(addr.line_1, 'address_line_1 → .line_1').toBe('100 Test Boulevard'); @@ -845,7 +891,9 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = const body = await capture_patch_body(page); - expect(body.attend_json?.zoom, 'attend_json.zoom is present').toBeTruthy(); + // attend_json is auto-serialized to a JSON string by update_ae_obj_v3. + const attend = JSON.parse(body.attend_json); + expect(attend?.zoom, 'attend_json.zoom is present').toBeTruthy(); expect(body.attend_url_code, 'Zoom meeting ID').toBe('82345678901'); expect(body.attend_url_passcode, 'Zoom passcode').toBe('mypassword'); }); @@ -928,17 +976,26 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = await page.locator('input[name="contact_1_email"]').fill('acarter@idaa-test.org'); await page.locator('input[name="contact_1_phone_mobile"]').fill('312-555-0001'); + // Contact 2 is collapsed by default when both contact_2.full_name and + // contact_2.email are null (as in mock_event). The collapsed {:else} block renders + // type="hidden" inputs — Playwright's fill() rejects these. Expand the section first. + const contact_2_toggle = page.getByRole('button', { name: /Contact 2 \(Optional\)/ }); + await contact_2_toggle.click(); + await expect(page.locator('input[name="contact_2_full_name"]')).toBeVisible({ timeout: 2000 }); + await page.locator('input[name="contact_2_full_name"]').fill('Dr. Bob Lee'); await page.locator('input[name="contact_2_email"]').fill('blee@idaa-test.org'); const body = await capture_patch_body(page); - expect(Array.isArray(body.contact_li_json), 'contact_li_json is an array').toBe(true); - expect(body.contact_li_json[0]?.full_name, 'contact 1 name').toBe('Dr. Alice Carter'); - expect(body.contact_li_json[0]?.email, 'contact 1 email').toBe('acarter@idaa-test.org'); - expect(body.contact_li_json[0]?.phone_mobile, 'contact 1 mobile').toBe('312-555-0001'); - expect(body.contact_li_json[1]?.full_name, 'contact 2 name').toBe('Dr. Bob Lee'); - expect(body.contact_li_json[1]?.email, 'contact 2 email').toBe('blee@idaa-test.org'); + // contact_li_json is auto-serialized to a JSON string by update_ae_obj_v3. + const contacts = JSON.parse(body.contact_li_json); + expect(Array.isArray(contacts), 'contact_li_json is an array').toBe(true); + expect(contacts[0]?.full_name, 'contact 1 name').toBe('Dr. Alice Carter'); + expect(contacts[0]?.email, 'contact 1 email').toBe('acarter@idaa-test.org'); + expect(contacts[0]?.phone_mobile, 'contact 1 mobile').toBe('312-555-0001'); + expect(contacts[1]?.full_name, 'contact 2 name').toBe('Dr. Bob Lee'); + expect(contacts[1]?.email, 'contact 2 email').toBe('blee@idaa-test.org'); }); // ------------------------------------------------------------------------- @@ -946,9 +1003,16 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = // ------------------------------------------------------------------------- test('admin fields (status, sort, group, hide, priority) are in PATCH payload', async ({ page }) => { - // Admin section is visible because trusted_access=true in test setup + // The Admin Options section is rendered (trusted_access=true) but collapsed by default + // (show_admin_options starts false). Collapsed state renders type="hidden" inputs — + // Playwright's fill() rejects these and isChecked() always returns false for hidden + // checkboxes. Expand the section before interacting with any admin fields. await open_edit_form(page); + const admin_toggle = page.getByRole('button', { name: /Admin Options/ }); + await admin_toggle.click(); + await expect(page.locator('input[name="status"]')).toBeVisible({ timeout: 2000 }); + await page.locator('input[name="status"]').fill('active'); await page.locator('input[name="sort"]').fill('30'); await page.locator('input[name="group"]').fill('International');