Files
OSIT-AE-App-Svelte/tests/badge_print_layout.test.ts

221 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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). */
async function inject_idb(badge: object, template: object): Promise<void> {
const db = await new Promise<IDBDatabase>((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<void>((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, [mock_badge, mock_template] as any);
// 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, [mock_badge, mock_template] as any);
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, [mock_badge, mock_template] as any);
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');
});
});