Badges: fix Firefox print centering (overflow:auto+display:contents); add print layout Playwright tests
This commit is contained in:
@@ -188,16 +188,31 @@
|
||||
/* Hide app chrome */
|
||||
.submenu { display: none !important; }
|
||||
|
||||
/* Dissolve layout wrappers — removes their boxes from the layout while
|
||||
keeping children renderable. The badge section floats up to body
|
||||
as a direct flex child, so body's centering applies to it directly.
|
||||
This avoids all height-chain and overflow-clip issues.
|
||||
/* Dissolve layout wrappers so .event_badge_wrapper bubbles up to a real
|
||||
centering flex container.
|
||||
|
||||
Layers dissolved:
|
||||
#ae_main_content — events layout scroll container (flex-col, max-w-7xl, overflow-auto, bg-gray)
|
||||
.main_content — events layout inner section (grow, pb-48, pt-20+, items-center)
|
||||
#badge_render_area — print page badge wrapper (screen-only right padding) */
|
||||
#ae_main_content,
|
||||
#ae_main_content has overflow:auto — per CSS spec, display:contents
|
||||
cannot override an element that establishes overflow clipping; Firefox
|
||||
(spec-compliant) keeps it as a block container, breaking centering.
|
||||
Fix: reset it as an explicit 100%×100% pass-through flex container
|
||||
instead. Chrome applies this too (belt-and-suspenders). */
|
||||
#ae_main_content {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: 100% !important;
|
||||
min-height: 0 !important;
|
||||
overflow: visible !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* .main_content and #badge_render_area have no overflow constraints,
|
||||
so display:contents works on all browsers — they dissolve their boxes
|
||||
and .event_badge_wrapper becomes a direct flex child of #ae_main_content. */
|
||||
.main_content,
|
||||
#badge_render_area {
|
||||
display: contents !important;
|
||||
@@ -221,8 +236,6 @@
|
||||
|
||||
/* ============================================================
|
||||
TEMPORARY DEBUG OUTLINES — remove before going live
|
||||
Each color = one layout layer so you can see what's taking
|
||||
up space and whether display:contents is actually dissolving.
|
||||
============================================================ */
|
||||
html {
|
||||
outline: 3px dashed lime !important;
|
||||
@@ -232,12 +245,13 @@
|
||||
outline: 4px solid blue !important;
|
||||
outline-offset: -4px !important;
|
||||
}
|
||||
/* Red = should be invisible (display:contents dissolves its box).
|
||||
If you see a red border, display:contents is NOT working here. */
|
||||
/* Red = #ae_main_content — now a real flex container (full width/height).
|
||||
Should be VISIBLE and same size as body. */
|
||||
#ae_main_content {
|
||||
outline: 3px dashed red !important;
|
||||
outline-offset: -6px !important;
|
||||
}
|
||||
/* Orange + purple = display:contents — should be INVISIBLE (no box). */
|
||||
.main_content {
|
||||
outline: 3px dashed orange !important;
|
||||
outline-offset: -9px !important;
|
||||
@@ -246,7 +260,7 @@
|
||||
outline: 3px dashed purple !important;
|
||||
outline-offset: -12px !important;
|
||||
}
|
||||
/* Cyan = the actual badge wrapper — should be the only visible box */
|
||||
/* Cyan = the actual badge — should be centered inside the red box */
|
||||
.event_badge_wrapper {
|
||||
outline: 3px solid cyan !important;
|
||||
outline-offset: 2px !important;
|
||||
|
||||
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