/** * Badge Print Page — Print Layout CSS Tests * * Verifies that the badge is horizontally (and vertically) centered on the page * when print media CSS is active, using Playwright's emulateMedia({ media: 'print' }). * * Strategy: inject badge + template data directly into IndexedDB so LiveQuery * fires without needing a full API round-trip, then check bounding boxes. * * Cross-browser: add Firefox to playwright.config.ts projects to catch the * overflow:auto + display:contents incompatibility that Firefox enforces strictly. */ import { test, expect } from '@playwright/test'; import { ae_app_local_data_defaults } from './_helpers/ae_defaults'; import { testing_event_id, testing_account_id, mock_site_domain } from './_helpers/env'; const event_id = testing_event_id; // Use the demo badge/template IDs from tests/README.md const badge_id = 'UIJT-73-63-61'; const template_id = 'jgfixEpYp1B'; // Minimal badge record — both *_id and *_id_random set to the same value so // db_events.badge.get(event_badge_id) finds the record via the Dexie primary key. const mock_badge = { id: badge_id, event_badge_id: badge_id, event_badge_id_random: badge_id, event_id: event_id, event_id_random: event_id, event_badge_template_id: template_id, event_badge_template_id_random: template_id, full_name_override: 'Scott Idem', given_name: 'Scott', family_name: 'Idem', email: 'scott@example.com', badge_type: 'presenter', badge_type_code: 'presenter', print_count: 0, affiliations: 'One Sky IT', location: 'Minneapolis, MN', enable: true, }; // Minimal template record — layout drives the @page size injection const mock_template = { id: template_id, id_random: template_id, event_badge_template_id: template_id, badge_template_id: template_id, event_id: event_id, event_id_random: event_id, name: 'Dev Demo 202x', layout: 'badge_3.5x5.5_pvc', cfg_json: '{}', enable: true, }; /** Injects records into ae_events_db IndexedDB tables via raw IDB API. * Must be called from page.evaluate() after the page has navigated (Dexie * schema is already initialised from the app's first open). * Single-argument form required: page.evaluate() passes exactly one arg. */ async function inject_idb({ badge, template }: { badge: object; template: object }): Promise { const db = await new Promise((resolve, reject) => { const req = indexedDB.open('ae_events_db'); req.onsuccess = () => resolve(req.result as IDBDatabase); req.onerror = () => reject((req as IDBRequest).error); }); await new Promise((resolve, reject) => { const tx = db.transaction(['badge', 'badge_template'], 'readwrite'); tx.objectStore('badge').put(badge); tx.objectStore('badge_template').put(template); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); reject(tx.error); }; }); } test.describe('Badge Print Page — print layout centering', () => { test.beforeEach(async ({ page }) => { page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`)); // Minimal API mocks — enough for the events layout to render without errors await page.route('**/v3/**', async (route) => { const url = route.request().url(); const method = route.request().method(); if (url.includes('site_domain/search')) { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [mock_site_domain] }) }); } // Return a minimal valid event so the layout header doesn't error if (url.includes(`/v3/crud/event/${event_id}`) && !url.includes('event_badge') && method === 'GET') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { id: event_id, event_id, name: 'Test Event', cfg_json: {}, mod_badges_json: {}, mod_pres_mgmt_json: {}, mod_abstracts_json: {}, mod_exhibits_json: {}, mod_meetings_json: {} } }) }); } // All other calls: empty list (no-op) return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }) }); }); // Seed localStorage auth so the app renders await page.addInitScript(([defaults, e_id, a_id]) => { const data = { ...defaults, account_id: a_id, allow_access: true, authenticated_access: true, trusted_access: true, manager_access: true, edit_mode: false, } as any; data.mod = { ...defaults.mod, events: { ...defaults.mod.events, event_id: e_id } }; window.localStorage.setItem('ae_loc', JSON.stringify(data)); }, [ae_app_local_data_defaults, event_id, testing_account_id] as const); }); test('badge is horizontally centered on a letter-size page', async ({ page }) => { // Letter paper: 8.5in × 11in at 96 dpi = 816 × 1056 px await page.setViewportSize({ width: 816, height: 1056 }); await page.goto(`/events/${event_id}/badges/${badge_id}/print`); await page.waitForLoadState('domcontentloaded'); // Inject badge + template directly into IndexedDB so LiveQuery fires await page.evaluate(inject_idb, { badge: mock_badge, template: mock_template }); // Wait for LiveQuery to render the badge wrapper await page.waitForSelector('.event_badge_wrapper', { timeout: 8000 }); // Apply print media CSS (activates @media print rules) await page.emulateMedia({ media: 'print' }); // Wait for layout to settle under print CSS await page.waitForFunction(() => { const el = document.querySelector('.event_badge_wrapper') as HTMLElement | null; return el !== null && el.offsetWidth > 0; }, { timeout: 5000 }); const badge_box = await page.locator('.event_badge_wrapper').boundingBox(); expect(badge_box, 'badge wrapper must have a bounding box').not.toBeNull(); const viewport = page.viewportSize()!; const badge_center_x = badge_box!.x + badge_box!.width / 2; const page_center_x = viewport.width / 2; // Badge horizontal center must be within 5px of the page center expect( Math.abs(badge_center_x - page_center_x), `badge center (${badge_center_x.toFixed(1)}px) should equal page center (${page_center_x}px)` ).toBeLessThan(5); }); test('badge is vertically centered on a letter-size page', async ({ page }) => { await page.setViewportSize({ width: 816, height: 1056 }); await page.goto(`/events/${event_id}/badges/${badge_id}/print`); await page.waitForLoadState('domcontentloaded'); await page.evaluate(inject_idb, { badge: mock_badge, template: mock_template }); await page.waitForSelector('.event_badge_wrapper', { timeout: 8000 }); await page.emulateMedia({ media: 'print' }); await page.waitForFunction(() => { const el = document.querySelector('.event_badge_wrapper') as HTMLElement | null; return el !== null && el.offsetHeight > 0; }, { timeout: 5000 }); const badge_box = await page.locator('.event_badge_wrapper').boundingBox(); expect(badge_box).not.toBeNull(); const viewport = page.viewportSize()!; const badge_center_y = badge_box!.y + badge_box!.height / 2; const page_center_y = viewport.height / 2; // Badge vertical center must be within 5px of the page center expect( Math.abs(badge_center_y - page_center_y), `badge center Y (${badge_center_y.toFixed(1)}px) should equal page center Y (${page_center_y}px)` ).toBeLessThan(5); }); test('#ae_main_content is a full-page positioned block in print mode', async ({ page }) => { // Verifies overflow:visible (overflow:auto fix) and position:relative (centering context). await page.goto(`/events/${event_id}/badges/${badge_id}/print`); await page.waitForLoadState('domcontentloaded'); await page.emulateMedia({ media: 'print' }); const styles = await page.evaluate(() => { const el = document.getElementById('ae_main_content'); if (!el) return null; const cs = getComputedStyle(el); return { overflow: cs.overflow, position: cs.position }; }); expect(styles, '#ae_main_content must exist').not.toBeNull(); expect(styles!.overflow, '#ae_main_content overflow should be visible in print').toBe('visible'); expect(styles!.position, '#ae_main_content should be positioned (relative) in print').toBe('relative'); }); test('badge wrapper is absolutely positioned and centered in print mode', async ({ page }) => { await page.setViewportSize({ width: 816, height: 1056 }); await page.goto(`/events/${event_id}/badges/${badge_id}/print`); await page.waitForLoadState('domcontentloaded'); await page.evaluate(inject_idb, { badge: mock_badge, template: mock_template }); await page.waitForSelector('.event_badge_wrapper', { timeout: 8000 }); await page.emulateMedia({ media: 'print' }); const styles = await page.evaluate(() => { const el = document.querySelector('.event_badge_wrapper') as HTMLElement | null; if (!el) return null; const cs = getComputedStyle(el); return { position: cs.position, transform: cs.transform, }; }); expect(styles).not.toBeNull(); expect(styles!.position, 'badge wrapper should be absolutely positioned').toBe('absolute'); // transform should contain a matrix (translate is set) expect(styles!.transform, 'badge wrapper should have a transform applied').not.toBe('none'); }); });