Files
OSIT-AE-App-Svelte/tests/event_badge_print_layout.test.ts
2026-03-24 11:15:01 -04:00

184 lines
8.1 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 { testing_event_id } from './_helpers/env';
import { inject_badge_and_template } from './_helpers/idb_helpers';
import { setup_badge_test_page } from './_helpers/minimal_ae_api_mocks';
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,
};
test.describe('Badge Print Page — print layout centering', () => {
test.beforeEach(async ({ page }) => {
await setup_badge_test_page(page, event_id);
});
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 });
// First navigation initializes the Dexie schema; inject_idb then writes the data.
// After injection we reload so liveQuery starts fresh against the populated IDB —
// direct IDB writes bypass Dexie's notification system and won't trigger a re-query.
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
await page.waitForLoadState('domcontentloaded');
await page.evaluate(inject_badge_and_template, { badge: mock_badge, template: mock_template });
await page.reload();
await page.waitForLoadState('domcontentloaded');
// 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_badge_and_template, { badge: mock_badge, template: mock_template });
await page.reload();
await page.waitForLoadState('domcontentloaded');
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`);
// domcontentloaded is too early — wait for the element to exist after hydration.
await page.waitForSelector('#ae_main_content', { timeout: 8000 });
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');
// position:static — print CSS strips #ae_main_content's overflow clipping by setting
// display:block + overflow:visible + position:static so it doesn't interfere with centering.
expect(styles!.position, '#ae_main_content should be static in print').toBe('static');
});
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_badge_and_template, { badge: mock_badge, template: mock_template });
await page.reload();
await page.waitForLoadState('domcontentloaded');
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();
// position:fixed — anchors to the @page content area in print, bypassing all ancestor
// overflow/height constraints. This is intentional (see print layout architecture docs).
expect(styles!.position, 'badge wrapper should be fixed in print').toBe('fixed');
// transform should contain a matrix (translate(-50%,-50%) for centering)
expect(styles!.transform, 'badge wrapper should have a transform applied').not.toBe('none');
});
});