// @ts-check // Test: LD1 — the PhotoSwipe slide's declared dimensions must match what the // browser actually renders for the linked image (BUG 2026-07-09: portrait // iPhone JPEGs squeezed to landscape in the fullscreen lightbox). // // Root cause: entry-journal.html.twig fed `img.width`/`img.height` (raw // getimagesize() of the ORIGINAL file — EXIF orientation ignored) into // data-pswp-*, while the slide href pointed at that original, which browsers // display EXIF-rotated. For a stored-landscape portrait photo the attrs said // landscape while the pixels rendered portrait → PhotoSwipe squeezed them. // // The invariant tested here is environment-proof: whatever file the slide // links to, its browser-rendered natural size must equal the data-pswp-* // attrs. (Whether the photo ALSO displays upright depends on the server's // php-exif extension feeding auto_fix_orientation — present on prod, absent // in the local dev container — so upright-ness is deliberately not asserted.) // // The fixture entry is planted straight on disk in the DEMO trip (the active // trip is whatever site.yaml says and may be an unpublished draft that 404s; // this spec exercises template rendering, not the posting pipeline — that is // upload-gate.spec.js's job). touch(system.yaml) bumps the config checksum so // the page-tree index rebuilds — the same invalidation cache-on-save uses. const { test, expect } = require('@playwright/test'); const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); // Stored 800x600 with EXIF Orientation=6: browsers render it 600x800 portrait. const EXIF_PORTRAIT = path.join(__dirname, '../../fixtures/test-photo-exif-portrait.jpg'); const USER_DIR = path.join(__dirname, '../../../user'); const DEMO_DAILIES = path.join(USER_DIR, 'pages/01.trips/italy-2026-demo/01.dailies'); const DEMO_TRIP_URL = '/trips/italy-2026-demo'; const TAG = `ld1-fixture-${Date.now()}`; const ENTRY_DIR = path.join(DEMO_DAILIES, `2026-09-30-1200-${TAG}.entry`); function bumpPageTreeIndex() { // mtime bump on system.yaml changes config->checksum(), which keys the // pages index — next request rebuilds the tree from disk. execSync(`touch "${path.join(USER_DIR, 'config/system.yaml')}"`); } test.beforeAll(() => { fs.mkdirSync(ENTRY_DIR, { recursive: true }); fs.copyFileSync(EXIF_PORTRAIT, path.join(ENTRY_DIR, 'photo-01.jpg')); fs.writeFileSync(path.join(ENTRY_DIR, 'entry.md'), [ '---', `title: 'UI Test ${TAG}'`, "date: '2026-09-30 12:00'", 'template: entry', 'published: true', '---', '', `Lightbox dims fixture ${TAG}. Safe to delete.`, '', ].join('\n')); bumpPageTreeIndex(); }); test.afterAll(() => { fs.rmSync(ENTRY_DIR, { recursive: true, force: true }); bumpPageTreeIndex(); }); test('LD1: lightbox slide dims match the rendered size of the linked image', async ({ page }) => { const card = page.locator('.journal-post', { hasText: TAG }); const slide = card.locator('a.journal-photo-slide').first(); // The config-checksum bump has second-granularity mtimes; a goto in the // same second can still be served the stale cached page. Reload until the // planted card is in the rendered feed. await expect(async () => { await page.goto(DEMO_TRIP_URL); await expect(slide).toBeAttached({ timeout: 1000 }); }).toPass({ timeout: 20_000 }); const attrW = Number(await slide.getAttribute('data-pswp-width')); const attrH = Number(await slide.getAttribute('data-pswp-height')); const href = await slide.getAttribute('href'); expect(attrW).toBeGreaterThan(0); expect(attrH).toBeGreaterThan(0); const natural = await page.evaluate((src) => new Promise((resolve, reject) => { const i = new Image(); i.onload = () => resolve({ w: i.naturalWidth, h: i.naturalHeight }); i.onerror = () => reject(new Error('image failed to load: ' + src)); i.src = src; }), href); expect(natural.w, `data-pswp-width vs rendered width of ${href}`).toBe(attrW); expect(natural.h, `data-pswp-height vs rendered height of ${href}`).toBe(attrH); });