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 */
|
/* Hide app chrome */
|
||||||
.submenu { display: none !important; }
|
.submenu { display: none !important; }
|
||||||
|
|
||||||
/* Dissolve layout wrappers — removes their boxes from the layout while
|
/* Dissolve layout wrappers so .event_badge_wrapper bubbles up to a real
|
||||||
keeping children renderable. The badge section floats up to body
|
centering flex container.
|
||||||
as a direct flex child, so body's centering applies to it directly.
|
|
||||||
This avoids all height-chain and overflow-clip issues.
|
|
||||||
|
|
||||||
Layers dissolved:
|
#ae_main_content has overflow:auto — per CSS spec, display:contents
|
||||||
#ae_main_content — events layout scroll container (flex-col, max-w-7xl, overflow-auto, bg-gray)
|
cannot override an element that establishes overflow clipping; Firefox
|
||||||
.main_content — events layout inner section (grow, pb-48, pt-20+, items-center)
|
(spec-compliant) keeps it as a block container, breaking centering.
|
||||||
#badge_render_area — print page badge wrapper (screen-only right padding) */
|
Fix: reset it as an explicit 100%×100% pass-through flex container
|
||||||
#ae_main_content,
|
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,
|
.main_content,
|
||||||
#badge_render_area {
|
#badge_render_area {
|
||||||
display: contents !important;
|
display: contents !important;
|
||||||
@@ -221,8 +236,6 @@
|
|||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
TEMPORARY DEBUG OUTLINES — remove before going live
|
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 {
|
html {
|
||||||
outline: 3px dashed lime !important;
|
outline: 3px dashed lime !important;
|
||||||
@@ -232,12 +245,13 @@
|
|||||||
outline: 4px solid blue !important;
|
outline: 4px solid blue !important;
|
||||||
outline-offset: -4px !important;
|
outline-offset: -4px !important;
|
||||||
}
|
}
|
||||||
/* Red = should be invisible (display:contents dissolves its box).
|
/* Red = #ae_main_content — now a real flex container (full width/height).
|
||||||
If you see a red border, display:contents is NOT working here. */
|
Should be VISIBLE and same size as body. */
|
||||||
#ae_main_content {
|
#ae_main_content {
|
||||||
outline: 3px dashed red !important;
|
outline: 3px dashed red !important;
|
||||||
outline-offset: -6px !important;
|
outline-offset: -6px !important;
|
||||||
}
|
}
|
||||||
|
/* Orange + purple = display:contents — should be INVISIBLE (no box). */
|
||||||
.main_content {
|
.main_content {
|
||||||
outline: 3px dashed orange !important;
|
outline: 3px dashed orange !important;
|
||||||
outline-offset: -9px !important;
|
outline-offset: -9px !important;
|
||||||
@@ -246,7 +260,7 @@
|
|||||||
outline: 3px dashed purple !important;
|
outline: 3px dashed purple !important;
|
||||||
outline-offset: -12px !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 {
|
.event_badge_wrapper {
|
||||||
outline: 3px solid cyan !important;
|
outline: 3px solid cyan !important;
|
||||||
outline-offset: 2px !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