210 lines
9.2 KiB
TypeScript
210 lines
9.2 KiB
TypeScript
/**
|
||
* 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 does not clip in print mode', async ({ page }) => {
|
||
// Verifies that overflow:visible is applied (the Firefox fix).
|
||
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
|
||
await page.waitForLoadState('domcontentloaded');
|
||
await page.emulateMedia({ media: 'print' });
|
||
|
||
const overflow = await page.evaluate(() => {
|
||
const el = document.getElementById('ae_main_content');
|
||
return el ? getComputedStyle(el).overflow : null;
|
||
});
|
||
expect(overflow, '#ae_main_content overflow should be visible in print').toBe('visible');
|
||
});
|
||
|
||
test('body is a flex container spanning full viewport width 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.emulateMedia({ media: 'print' });
|
||
|
||
const result = await page.evaluate(() => ({
|
||
display: getComputedStyle(document.body).display,
|
||
width: document.body.offsetWidth,
|
||
}));
|
||
|
||
expect(result.display, 'body should be flex in print').toBe('flex');
|
||
// Body width must be close to the viewport width (no shrink-to-content)
|
||
expect(result.width, 'body should span full viewport width').toBeGreaterThanOrEqual(810);
|
||
});
|
||
});
|