Badges: fix Firefox print centering (overflow:auto+display:contents); add print layout Playwright tests
This commit is contained in:
209
tests/badge_print_layout.test.ts
Normal file
209
tests/badge_print_layout.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user