diff --git a/tests/README.md b/tests/README.md index f49d875c..397453d3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -67,5 +67,11 @@ Help * Aether test/demo Event Presentation: '7U2eXSjR6H4' (1670) "Build a House" * Aether test/demo Event Presenter: 'gT-hxnifb-0' (2202) "Bob The Builder" * Aether test/demo Event File: 'OOsHXtng5mr' (2985) "1 Quick Test for macOS.mp4" +* Aether test/demo Event Badge: 'UIJT-73-63-61' (37163) "Scott Idem" +* Aether test/demo Event Person: 'ffkKxiHpOEC' (16603) "Scott Idem" +* Aether test/demo Event Badge Template: 'jgfixEpYp1B' (18) "Dev Demo 202x" +* Aether test/demo Event Badge Template: 'rzmUgsk7mkq' (19) "Dev Demo 202x Workshops" * Aether test/demo Journal: 'BVYE-94-46-29' (42) "Testing Things" * Aether test/demo Journal Entry: 'xRx-Y4-h3-fU' (233) "Another Journal Entry in the Test Journal" +* Aether test/demo Archive: 'nAA2bHLv8RK' (1) "One Sky Test Archive" +* Aether test/demo Archive Content: 'UjKzrk-GKu5' (1) "Hosted File Test" diff --git a/tests/coldstart_event_session.test.ts b/tests/coldstart_event_session.test.ts new file mode 100644 index 00000000..fb9d5d7e --- /dev/null +++ b/tests/coldstart_event_session.test.ts @@ -0,0 +1,219 @@ +import { test, expect } from '@playwright/test'; +import { ae_app_local_data_defaults } from './_helpers/ae_defaults'; +import { demo_event_id, demo_account_id } from './_helpers/env'; + +// Demo test data IDs from One Sky IT Demo environment +// Session: (703) "How To Do Things" +// Presentation: (1670) "Build a House" +// Presenter: (2202) "Bob The Builder" +const demo_session_id = 'DOW3h7v6H42'; +const demo_presentation_id = '7U2eXSjR6H4'; +const demo_presenter_id = 'gT-hxnifb-0'; + +test.describe('Cold-start: Event Session (IndexedDB empty)', () => { + test.beforeEach(async ({ page }) => { + page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); + page.on('console', (msg) => { + if (msg.type() === 'error' || msg.type() === 'warn') + console.error(`BROWSER [${msg.type().toUpperCase()}]: ${msg.text()}`); + }); + + // Provide app localStorage before any scripts run + await page.addInitScript( + ({ defaults, event_id, account_id }) => { + const testData = { + ...defaults, + account_id: account_id, + administrator_access: true, + trusted_access: true, + authenticated_access: true, + manager_access: false, + super_access: false, + edit_mode: true, + mod_abstracts_json: {}, + person_id: 'test-person-1', + user: { id: 'test-person-1' }, + mod: { ...defaults.mod, events: { ...defaults.mod.events, event_id: event_id } } + }; + window.localStorage.setItem('ae_loc', JSON.stringify(testData)); + }, + { defaults: ae_app_local_data_defaults, event_id: demo_event_id, account_id: demo_account_id } + ); + + // Navigate to the application's origin so the page context is allowed + // to access the IndexedDB API, then delete known Dexie DBs to simulate + // a cold start. + await page.goto('/'); + await page.evaluate(() => { + const dbs = [ + 'ae_events_db', + 'ae_journals_db', + 'ae_posts_db', + 'ae_archives_db', + 'ae_core_db', + 'ae_sponsorships_db' + ]; + return Promise.all( + dbs.map((name) => + new Promise((resolve) => { + try { + const req = indexedDB.deleteDatabase(name); + req.onsuccess = () => resolve(true); + req.onerror = () => resolve(false); + req.onblocked = () => resolve(false); + } catch (e) { + resolve(false); + } + }) + ) + ); + }); + + // Network mock: minimal responses for session, presentations, presenters + await page.route('**/v3/crud/**', async (route) => { + const req = route.request(); + const url = req.url(); + const method = req.method(); + + // Session GET + if (url.includes(`/v3/crud/event_session/${demo_session_id}`) && method === 'GET') { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: { + id: demo_session_id, + event_session_id: demo_session_id, + event_id: demo_event_id, + name: 'How To Do Things', + code: 'how-to-do-things', + start_datetime: '2026-03-01T10:00:00Z', + end_datetime: '2026-03-01T11:00:00Z', + description: 'Cold start test session', + event_location_id: 'test-location-1', + poc_person_id: 'test-person-1', + cfg_json: {}, + mod_pres_mgmt_json: {} + } + }) + }); + } + + // Presentations list for session + if (url.includes('/v3/crud/event_presentation') && url.includes('for_obj_id') && url.includes(demo_session_id) && method === 'GET') { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: [ + { + id: demo_presentation_id, + event_presentation_id: demo_presentation_id, + event_session_id: demo_session_id, + event_id: demo_event_id, + name: 'Build a House', + code: 'build-house', + start_datetime: '2026-03-01T10:00:00Z', + end_datetime: '2026-03-01T10:30:00Z' + } + ] + }) + }); + } + + // Presenters for "Build a House" presentation + if (url.includes('/v3/crud/event_presenter') && url.includes(`for_obj_id=${demo_presentation_id}`) && method === 'GET') { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: [ + { + id: demo_presenter_id, + event_presenter_id: demo_presenter_id, + event_presentation_id: demo_presentation_id, + event_session_id: demo_session_id, + event_id: demo_event_id, + given_name: 'Bob', + family_name: 'The Builder', + full_name: 'Bob The Builder', + email: 'bob@builder.example.com' + } + ] + }) + }); + } + + // Default: empty envelope + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: [] }) + }); + }); + }); + + test('renders Session with Presentations and Presenters on first load', async ({ page }) => { + // Navigate directly to the session page (cold start - no IDB data) + await page.goto(`/events/${demo_event_id}/session/${demo_session_id}`); + + // 1. Verify session name is visible + await expect(page.getByText('How To Do Things')).toBeVisible({ timeout: 10000 }); + + // 2. Verify presentation is visible + await expect(page.getByText('Build a House')).toBeVisible({ timeout: 5000 }); + + // 3. CRITICAL: Verify presenter is visible WITHOUT manual refresh + // This is the bug we're fixing - presenters should appear on first load + await expect(page.getByText('Bob The Builder')).toBeVisible({ timeout: 10000 }); + + // 4. Verify the presentations count display + const presCountBadge = page.locator('text=/Presentations:/').locator('..').getByText('1'); + await expect(presCountBadge).toBeVisible({ timeout: 5000 }); + }); + + test('verifies IndexedDB contains all nested data after load', async ({ page }) => { + await page.goto(`/events/${demo_event_id}/session/${demo_session_id}`); + + // Wait for data to be visible (indicating IDB writes completed) + await expect(page.getByText('Bob The Builder')).toBeVisible({ timeout: 10000 }); + + // Inspect IndexedDB to verify data integrity + const idbData = await page.evaluate(async () => { + const openDB = (name: string) => new Promise((resolve, reject) => { + const req = indexedDB.open(name); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + + const getAllFromStore = (db: IDBDatabase, storeName: string) => new Promise((resolve) => { + const tx = db.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + const req = store.getAll(); + req.onsuccess = () => resolve(req.result); + req.onerror = () => resolve([]); + }); + + try { + const db = await openDB('ae_events_db'); + const sessions = await getAllFromStore(db, 'session'); + const presentations = await getAllFromStore(db, 'presentation'); + const presenters = await getAllFromStore(db, 'presenter'); + db.close(); + + return { sessions, presentations, presenters }; + } catch (e) { + return { sessions: [], presentations: [], presenters: [], error: String(e) }; + } + }); + + // Verify data was written to IDB + expect(idbData.sessions.length).toBeGreaterThan(0); + expect(idbData.presentations.length).toBe(1); + expect(idbData.presenters.length).toBe(1); + + // Verify presenter name is in IDB + const presenterNames = idbData.presenters.map((p: any) => p.full_name); + expect(presenterNames).toContain('Bob The Builder'); + }); +});