CRITICAL IDENTITY FIX: Ensures all member-generated content (Meetings, Posts, Comments) is explicitly linked to the creator's Novi UUID via 'external_person_id' at the moment of creation. Changes: - Added 'external_person_id' to creation payloads in Recovery Meetings and BB Posts. - Implemented 'identity scavenging' from localStorage in submit handlers to prevent race conditions where Svelte stores are briefly null. - Refactored Post Comment edit component to robustly initialize and save creator identity. - Added 'The Novi UUID Rule' to IDAA documentation to mandate this pattern for future development. - Added Playwright test to verify creation linkage and fixed a version-mismatch bug in the test auth helper. Note: Archives and Archive Content are excluded as they do not require member ownership.
1083 lines
49 KiB
TypeScript
1083 lines
49 KiB
TypeScript
/**
|
||
* 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: '<p>Main entrance on Oak Street.</p>',
|
||
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 <select required>
|
||
// with a real value rather than the fallback <input required value="">.
|
||
// Without this, $ae_loc.lu_time_zone_list is empty on first mount and
|
||
// the <input> 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<void>((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 <label class="day-chip"> intercepts all pointer events.
|
||
// Click the label (the visible chip), not the hidden input.
|
||
const wednesday_cb = page.locator('input[name="weekday_wednesday"]');
|
||
const wednesday_label = page.locator('label[for="weekday_wednesday"]');
|
||
const initial_state = await wednesday_cb.isChecked();
|
||
|
||
await wednesday_label.click();
|
||
await expect(wednesday_cb).toBeChecked({ checked: !initial_state });
|
||
|
||
// Toggle back
|
||
await wednesday_label.click();
|
||
await expect(wednesday_cb).toBeChecked({ checked: initial_state });
|
||
});
|
||
|
||
test('physical checkbox shows/hides address section', async ({ page }) => {
|
||
await page.goto(`/`);
|
||
await seed_event_idb(page, mock_event); // physical=true
|
||
await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`);
|
||
|
||
await page.waitForSelector('section.edit__event_obj', { timeout: 5000 });
|
||
|
||
// v2 uses a <div class="subsection" class:hidden={!physical ...}> rather
|
||
// than a <fieldset id="physical_address"> (that was v1). The address
|
||
// inputs are always in the DOM; visibility is driven by the Svelte
|
||
// class:hidden binding. With physical=true in mock_event the div is
|
||
// NOT hidden, so address_name should be visible once liveQuery settles.
|
||
//
|
||
// Physical/virtual checkboxes are also sr-only — click the parent label.
|
||
const physical_label = page.locator('label[for="physical"]');
|
||
const address_name_inp = page.locator('input[name="address_name"]');
|
||
|
||
await expect(physical_label).toBeVisible({ timeout: 5000 });
|
||
|
||
// mock_event has physical=true so the address section should already be
|
||
// visible after liveQuery settles.
|
||
await expect(address_name_inp).toBeVisible({ timeout: 5000 });
|
||
|
||
// Toggling physical off should hide the address section (input becomes hidden)
|
||
const physical_cb = page.locator('input[name="physical"]');
|
||
await physical_label.click();
|
||
await expect(physical_cb).toBeChecked({ checked: false });
|
||
await expect(address_name_inp).not.toBeVisible({ timeout: 3000 });
|
||
|
||
// Restore: toggle physical back on
|
||
await physical_label.click();
|
||
await expect(physical_cb).toBeChecked({ checked: true });
|
||
});
|
||
|
||
test('start time field accepts a time value', 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 start_time = page.locator('input[name="recurring_start_time"]');
|
||
await start_time.fill('19:30');
|
||
await expect(start_time).toHaveValue('19:30');
|
||
});
|
||
|
||
test('contact_1 full 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 contact_name = page.locator('input[name="contact_1_full_name"]');
|
||
await contact_name.fill('Jane Smith');
|
||
await expect(contact_name).toHaveValue('Jane Smith');
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 3: Submit behavior
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('save button is present and clickable', 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 });
|
||
|
||
// The form's submit button — visible at the bottom of the form
|
||
const save_btn = page.locator('button[type="submit"]').filter({ hasText: /save|submit/i }).first();
|
||
await expect(save_btn).toBeVisible();
|
||
await expect(save_btn).toBeEnabled();
|
||
});
|
||
|
||
test('form submission sends PATCH request to event API', 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 });
|
||
|
||
// Track API calls
|
||
const api_calls: Array<{ url: string; method: string }> = [];
|
||
page.on('request', (req: any) => {
|
||
if (req.url().includes('/v3/') && req.method() === 'PATCH') {
|
||
api_calls.push({ url: req.url(), method: req.method() });
|
||
}
|
||
});
|
||
|
||
// Make a visible change then submit
|
||
const name_input = page.locator('input[name="name"]');
|
||
await name_input.fill('Updated Thursday Group');
|
||
|
||
const save_btn = page.locator('button[type="submit"]').filter({ hasText: /save|submit/i }).first();
|
||
await save_btn.click();
|
||
|
||
// Wait briefly for the async submit to fire
|
||
await page.waitForTimeout(1500);
|
||
|
||
// Expect a PATCH was made to the event endpoint
|
||
const patch_to_event = api_calls.some(c => c.url.includes(`/v3/crud/event/${TEST_EVENT_ID}`));
|
||
expect(patch_to_event, 'Expected PATCH request to event API endpoint').toBe(true);
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 4: Virtual / Zoom platform section
|
||
// -------------------------------------------------------------------------
|
||
|
||
|
||
test('virtual checkbox reveals virtual/platform section', async ({ page }) => {
|
||
await page.goto(`/`);
|
||
// Use a virtual-only event
|
||
const virtual_event = {
|
||
...mock_event,
|
||
id: TEST_EVENT_ID,
|
||
physical: false,
|
||
virtual: true,
|
||
attend_json: { zoom: null }
|
||
};
|
||
await seed_event_idb(page, virtual_event);
|
||
await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`);
|
||
|
||
await page.waitForSelector('section.edit__event_obj', { timeout: 5000 });
|
||
|
||
// The virtual checkbox (id="virtual") should be present and checked
|
||
const virtual_cb = page.locator('input#virtual');
|
||
await expect(virtual_cb).toBeAttached();
|
||
});
|
||
|
||
});
|
||
|
||
// =============================================================================
|
||
// Integration tests — real backend
|
||
// These tests use the real event ID and let CRUD calls reach the actual API.
|
||
// They will modify real data in the database.
|
||
// =============================================================================
|
||
|
||
test.describe('IDAA Recovery Meetings — Real Backend Save (Integration)', () => {
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
page.on('pageerror', (err: Error) =>
|
||
console.error(`BROWSER ERROR: ${err.message}`)
|
||
);
|
||
await setup_idaa_auth(page);
|
||
// pass_through_event_patch=true: the PATCH reaches the real backend.
|
||
// pass_through_site_domain=true: the app fetches real credentials from
|
||
// dev-api.oneskyit.com so that x-aether-api-key is valid for the PATCH.
|
||
// pass_through_lookups=true: country/subdivision data comes from the real
|
||
// API so the submit handler can resolve subdivision codes to names when
|
||
// building location_address_json. Without this, the mock 2-entry list
|
||
// may be missing the address code and cause a body validation error (400).
|
||
await setup_api_mocks(page, TEST_EVENT_ID, {
|
||
pass_through_event_patch: true,
|
||
pass_through_site_domain: true,
|
||
pass_through_lookups: true
|
||
});
|
||
});
|
||
|
||
test('save updated meeting name persists to backend', async ({ page }) => {
|
||
// Seed IDB so the form populates immediately without waiting for the real API GET.
|
||
// The mock_event name will appear in the field; we overwrite it before saving.
|
||
await page.goto(`/`);
|
||
await seed_event_idb(page, mock_event);
|
||
await page.goto(`/idaa/recovery_meetings/${TEST_EVENT_ID}`);
|
||
|
||
// Wait for the form field to be populated from IDB via liveQuery
|
||
const name_input = page.locator('input[name="name"]');
|
||
await expect(name_input).toBeVisible({ timeout: 5000 });
|
||
await expect(name_input).not.toHaveValue('', { timeout: 5000 });
|
||
|
||
// Read the current name and append a test marker (idempotent: strip any previous marker first)
|
||
const current_name = await name_input.inputValue();
|
||
const base_name = current_name.replace(/ \[PW\]$/, '').trim();
|
||
const new_name = `${base_name} [PW]`;
|
||
|
||
await name_input.fill(new_name);
|
||
|
||
const save_btn = page.locator('button[type="submit"]').filter({ hasText: /save/i }).first();
|
||
await expect(save_btn).toBeEnabled();
|
||
|
||
// Click Save and wait for the real PATCH response
|
||
const [response] = await Promise.all([
|
||
page.waitForResponse(
|
||
(resp) =>
|
||
resp.url().includes(`/v3/crud/event/${TEST_EVENT_ID}`) &&
|
||
resp.request().method() === 'PATCH',
|
||
{ timeout: 5000 }
|
||
),
|
||
save_btn.click()
|
||
]);
|
||
|
||
const body = await response.json();
|
||
expect(response.status(), 'PATCH should return HTTP 200').toBe(200);
|
||
expect(body?.data?.name ?? body?.data?.event_id, 'Response should include event data').toBeTruthy();
|
||
});
|
||
|
||
});
|
||
|
||
// =============================================================================
|
||
// Field save payload verification tests
|
||
// These tests verify that each section of the edit form correctly assembles
|
||
// and sends its fields in the PATCH request body. They catch silent bugs
|
||
// where a field renders but its value is dropped or mis-keyed on submit.
|
||
// All tests use mocked API responses — no real backend traffic.
|
||
// =============================================================================
|
||
|
||
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) =>
|
||
console.error(`BROWSER ERROR: ${err.message}`)
|
||
);
|
||
await setup_idaa_auth(page);
|
||
// pass_through_lookups: the real API returns 50+ entries for every
|
||
// lookup table, which naturally satisfies the component's "> 50 entries"
|
||
// cache-hit threshold. No need for 55-entry mock fixture arrays.
|
||
// Event PATCH is still intercepted so capture_patch_body can inspect the
|
||
// payload without actually writing test data to the backend on every run.
|
||
// pass_through_site_domain: ensures $ae_api gets real credentials so the
|
||
// SvelteKit load chain completes, ae_acct is populated, and the $effect
|
||
// sets $idaa_slct.event_id — without it the submit handler does POST
|
||
// (create) instead of PATCH (update) and capture_patch_body times out.
|
||
await setup_api_mocks(page, TEST_EVENT_ID, {
|
||
pass_through_lookups: true,
|
||
pass_through_site_domain: true
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Navigate to the edit form and wait until both the section is visible AND
|
||
* the liveQuery has populated the form from IDB.
|
||
*
|
||
* The form inputs use Svelte 5's one-way `value={$lq__event_obj?.field}`
|
||
* binding. If a test fills a field before liveQuery resolves, the next
|
||
* reactive tick resets the input back to the IDB value — producing wrong or
|
||
* undefined data in the PATCH body. Waiting for the name field to have a
|
||
* non-empty value confirms liveQuery is settled and no further reactive
|
||
* resets will occur.
|
||
*/
|
||
async function open_edit_form(page: any): Promise<void> {
|
||
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 });
|
||
// Wait for liveQuery to have populated the name field before any fills.
|
||
await page.waitForFunction(
|
||
() => {
|
||
const name = document.querySelector('input[name="name"]') as HTMLInputElement | null;
|
||
return name !== null && name.value.length > 0;
|
||
},
|
||
{ timeout: 5000 }
|
||
);
|
||
// Wait for the country subdivision and country lookup lists to finish loading.
|
||
// The submit handler resolves address codes (e.g. 'US-IL' → 'Illinois', 'US' →
|
||
// 'United States') from these lists when building location_address_json. If
|
||
// either list is empty at Save time the handler throws a TypeError and no PATCH
|
||
// is sent. Both fetches fire at component mount; we wait for each to reach
|
||
// ≥50 entries in localStorage (which happens in the same .then() as the $state
|
||
// update, so once localStorage reflects it the in-memory state is also ready).
|
||
await page.waitForFunction(
|
||
() => {
|
||
try {
|
||
const sub = localStorage.getItem('lu_country_subdivision_list');
|
||
const cty = localStorage.getItem('lu_country_list');
|
||
return (
|
||
sub !== null && JSON.parse(sub).length > 50 &&
|
||
cty !== null && JSON.parse(cty).length > 50
|
||
);
|
||
} catch { return false; }
|
||
},
|
||
{ timeout: 10000 }
|
||
);
|
||
// Wait for the timezone select to have a value. setup_idaa_auth pre-seeds
|
||
// lu_time_zone_list in ae_loc so the <select required> always renders (never
|
||
// the fallback <input required value="">). The value comes from
|
||
// $lq__event_obj?.timezone (IDB) or the pre-seeded current_timezone fallback.
|
||
await page.waitForFunction(
|
||
() => {
|
||
const tz = document.querySelector('section.edit__event_obj [name="timezone"]') as HTMLInputElement | HTMLSelectElement | null;
|
||
return tz !== null && tz.value.length > 0;
|
||
},
|
||
{ 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 }
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Click the Save button and capture the first PATCH request to the event
|
||
* endpoint. Returns the parsed JSON request body.
|
||
*
|
||
* We use waitForRequest (not waitForResponse) so we get the outgoing payload
|
||
* immediately without waiting for the mocked response round-trip.
|
||
*/
|
||
async function capture_patch_body(page: any): Promise<Record<string, any>> {
|
||
const req_promise = page.waitForRequest(
|
||
(r: any) =>
|
||
r.url().includes(`/v3/crud/event/${TEST_EVENT_ID}`) &&
|
||
r.method() === 'PATCH',
|
||
{ timeout: 5000 }
|
||
);
|
||
await page.locator('button[type="submit"]').first().click();
|
||
const req = await req_promise;
|
||
return JSON.parse(req.postData() ?? '{}');
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 1 — Meeting Information (name, type)
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('name and meeting type are sent in PATCH payload', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
await page.locator('input[name="name"]').fill('Payload Verification Meeting');
|
||
await page.selectOption('select[name="type"]', 'Caduceus');
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
expect(body.name, 'name field').toBe('Payload Verification Meeting');
|
||
expect(body.type, 'type field').toBe('Caduceus');
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 2 — How to Attend (physical/virtual flags, address, virtual platform)
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('physical and virtual boolean flags are in PATCH payload', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
// mock_event: physical=true, virtual=false — enable virtual.
|
||
// Toggles use sr-only checkboxes; click the parent <label> (toggle-chip),
|
||
// not the hidden input.
|
||
const virtual_cb = page.locator('input#virtual');
|
||
const virtual_label = page.locator('label[for="virtual"]');
|
||
if (!(await virtual_cb.isChecked())) await virtual_label.click();
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
expect(body.physical, 'physical flag').toBe(true);
|
||
expect(body.virtual, 'virtual flag').toBe(true);
|
||
});
|
||
|
||
test('address fields are nested inside location_address_json in PATCH payload', async ({ page }) => {
|
||
// mock_event.physical = true so the address subsection is visible without any interaction
|
||
await open_edit_form(page);
|
||
|
||
await page.locator('input[name="address_name"]').fill('Test Community Hall');
|
||
await page.locator('input[name="address_line_1"]').fill('100 Test Boulevard');
|
||
await page.locator('input[name="address_city"]').fill('Madison');
|
||
await page.locator('input[name="address_postal_code"]').fill('53703');
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
// _json fields are auto-serialized to JSON strings by update_ae_obj 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');
|
||
expect(addr.city, 'address_city → .city').toBe('Madison');
|
||
expect(addr.postal_code, 'address_postal_code → .postal_code').toBe('53703');
|
||
});
|
||
|
||
test('Zoom virtual platform fields are saved in attend_json', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
// Enable virtual mode so the virtual subsection appears.
|
||
// Toggles use sr-only checkboxes; click the parent <label> (toggle-chip).
|
||
const virtual_cb = page.locator('input#virtual');
|
||
const virtual_label = page.locator('label[for="virtual"]');
|
||
if (!(await virtual_cb.isChecked())) await virtual_label.click();
|
||
|
||
// Click the Zoom platform selector button
|
||
const zoom_btn = page.getByRole('button', { name: 'Zoom' }).first();
|
||
await expect(zoom_btn).toBeVisible({ timeout: 3000 });
|
||
await zoom_btn.click();
|
||
|
||
// Fill Zoom fields; dispatch change to trigger the URL-builder $effect
|
||
const meeting_id_input = page.locator('input[name="attend_url_code"]');
|
||
await expect(meeting_id_input).toBeVisible({ timeout: 3000 });
|
||
await meeting_id_input.fill('82345678901');
|
||
await meeting_id_input.dispatchEvent('change');
|
||
|
||
const passcode_input = page.locator('input[name="attend_url_passcode"]');
|
||
await passcode_input.fill('mypassword');
|
||
await passcode_input.dispatchEvent('change');
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
// attend_json is auto-serialized to a JSON string by update_ae_obj.
|
||
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');
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 3 — Schedule (pattern, timezone, times, weekdays)
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('schedule fields (pattern, timezone, start/end times) are in PATCH payload', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
await page.selectOption('select[name="recurring_pattern"]', 'monthly');
|
||
|
||
// open_edit_form already waits for the subdivision list (> 50), which means
|
||
// the timezone list has also loaded. Pick a timezone that differs from
|
||
// mock_event.timezone ('US/Central') so we test an actual selection change.
|
||
// We read available options at runtime rather than hardcoding a value that
|
||
// might not exist in the real timezone table.
|
||
const tz_select = page.locator('select[name="timezone"]');
|
||
const tz_options = tz_select.locator('option');
|
||
await expect(tz_options).not.toHaveCount(0, { timeout: 5000 });
|
||
let target_tz = mock_event.timezone; // fallback: keep current value
|
||
const option_count = await tz_options.count();
|
||
for (let i = 0; i < Math.min(option_count, 20); i++) {
|
||
const val = (await tz_options.nth(i).getAttribute('value')) ?? '';
|
||
if (val && val !== mock_event.timezone) {
|
||
target_tz = val;
|
||
break;
|
||
}
|
||
}
|
||
await tz_select.selectOption(target_tz);
|
||
|
||
await page.locator('input[name="recurring_start_time"]').fill('09:00');
|
||
await page.locator('input[name="recurring_end_time"]').fill('10:30');
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
expect(body.recurring, 'recurring is hardcoded true').toBe(true);
|
||
expect(body.recurring_pattern, 'recurring_pattern').toBe('monthly');
|
||
expect(body.timezone, 'timezone field present in PATCH').toBe(target_tz);
|
||
expect(body.recurring_start_time, 'start time').toBe('09:00');
|
||
expect(body.recurring_end_time, 'end time').toBe('10:30');
|
||
});
|
||
|
||
test('all seven weekday checkboxes are individually correct in PATCH payload', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
// mock_event: only weekday_thursday=true — switch to Sunday + Saturday.
|
||
// Day-chip checkboxes are sr-only; click the parent <label> for each day.
|
||
if (await page.locator('input[name="weekday_thursday"]').isChecked()) {
|
||
await page.locator('label[for="weekday_thursday"]').click();
|
||
}
|
||
if (!(await page.locator('input[name="weekday_sunday"]').isChecked())) {
|
||
await page.locator('label[for="weekday_sunday"]').click();
|
||
}
|
||
if (!(await page.locator('input[name="weekday_saturday"]').isChecked())) {
|
||
await page.locator('label[for="weekday_saturday"]').click();
|
||
}
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
expect(body.weekday_sunday, 'weekday_sunday').toBe(true);
|
||
expect(body.weekday_monday, 'weekday_monday').toBe(false);
|
||
expect(body.weekday_tuesday, 'weekday_tuesday').toBe(false);
|
||
expect(body.weekday_wednesday, 'weekday_wednesday').toBe(false);
|
||
expect(body.weekday_thursday, 'weekday_thursday').toBe(false);
|
||
expect(body.weekday_friday, 'weekday_friday').toBe(false);
|
||
expect(body.weekday_saturday, 'weekday_saturday').toBe(true);
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 4 — Contacts (contact_li_json)
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('contact_li_json contains both contact entries in PATCH payload', async ({ page }) => {
|
||
await open_edit_form(page);
|
||
|
||
// Trusted user: contact_1_locked starts false, fields are editable
|
||
await page.locator('input[name="contact_1_full_name"]').fill('Dr. Alice Carter');
|
||
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);
|
||
|
||
// contact_li_json is auto-serialized to a JSON string by update_ae_obj.
|
||
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');
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Section 5 — Admin Options (trusted users only)
|
||
// -------------------------------------------------------------------------
|
||
|
||
test('admin fields (status, sort, group, hide, priority) are in PATCH payload', async ({ page }) => {
|
||
// 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');
|
||
|
||
// Both are false in mock_event — enable them
|
||
const hide_cb = page.locator('input[name="hide"]');
|
||
const priority_cb = page.locator('input[name="priority"]');
|
||
if (!(await hide_cb.isChecked())) await hide_cb.click();
|
||
if (!(await priority_cb.isChecked())) await priority_cb.click();
|
||
|
||
const body = await capture_patch_body(page);
|
||
|
||
expect(body.status, 'status').toBe('active');
|
||
expect(body.sort, 'sort is cast to Number').toBe(30);
|
||
expect(body.group, 'group').toBe('International');
|
||
expect(body.hide, 'hide').toBe(true);
|
||
expect(body.priority, 'priority').toBe(true);
|
||
});
|
||
|
||
});
|
||
|
||
// =============================================================================
|
||
// Creation and Identity Linkage Tests
|
||
// Verifies that new meetings are correctly linked to the creating member's Novi ID.
|
||
// =============================================================================
|
||
|
||
test.describe('IDAA Recovery Meetings — Creation and Identity', () => {
|
||
|
||
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);
|
||
});
|
||
|
||
test('creating a new meeting sends external_person_id in POST payload', async ({ page }) => {
|
||
// 1. Start at the meeting list page
|
||
await page.goto('/idaa/recovery_meetings');
|
||
|
||
// 2. Click "Create New Meeting" button
|
||
// The component uses window.confirm() — we must handle the dialog
|
||
page.on('dialog', dialog => dialog.accept());
|
||
|
||
// Track API calls to capture the POST request (Creation, NOT search)
|
||
const post_promise = page.waitForRequest(
|
||
(req: any) =>
|
||
req.url().includes('/v3/crud/event') &&
|
||
!req.url().includes('search') &&
|
||
req.method() === 'POST',
|
||
{ timeout: 5000 }
|
||
);
|
||
|
||
await page.getByRole('button', { name: /Create New Meeting/ }).click();
|
||
|
||
// 3. Capture and verify the POST body
|
||
const post_req = await post_promise;
|
||
const post_body = JSON.parse(post_req.postData() ?? '{}');
|
||
|
||
expect(post_body.external_person_id, 'external_person_id in POST body').toBe(TEST_NOVI_UUID);
|
||
expect(post_body.name, 'default meeting name').toContain('Recovery Meeting Name');
|
||
});
|
||
|
||
});
|
||
|