Tests: fix IDAA recovery meeting test suite

Multiple failures caused by the SWR background fetch pattern and
collapsed UI sections:

1. IDB settle wait: add waitForFunction that polls IndexedDB for
   tmp_sort_1 on the event record. tmp_sort_1 is only written by
   _process_generic_props, so its presence signals the background
   API fetch has completed and no further liveQuery re-fires will
   overwrite form inputs the test is about to fill.

2. JSON.parse for _json fields: update_ae_obj_v3 auto-serializes any
   key ending in _json to a JSON string before sending the PATCH.
   Tests now parse location_address_json, attend_json, and
   contact_li_json from the captured body before asserting field values.

3. Contact 2 section: collapsed by default when contact_2.full_name
   and email are null. Collapsed state renders type='hidden' inputs
   which Playwright's fill() rejects. Add click on the 'Contact 2
   (Optional)' toggle before filling those fields.

4. Admin Options section: same collapse pattern. Add click on the
   'Admin Options' toggle before filling status/sort/group/hide fields.

5. Increase suite timeout to 60 s: open_edit_form awaits real lookup
   API responses (pass_through_lookups=true) which can take 20-25 s
   on slow network, leaving no margin at the default 30 s limit.
This commit is contained in:
Scott Idem
2026-03-10 14:23:01 -04:00
parent 44d4b8e04f
commit 283ccb3ce4

View File

@@ -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
// 2025 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<boolean>((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');