// @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 feeds `img.width`/`img.height` (raw // getimagesize() of the ORIGINAL file — EXIF orientation ignored) into // data-pswp-*, while the slide href points at that original, which browsers // display EXIF-rotated. For a stored-landscape portrait photo the attrs say // landscape while the pixels render portrait → PhotoSwipe squeezes them. // // ⚠️ THIS SPEC CURRENTLY FAILS — the root cause above is still live. // entry-journal.html.twig:48-49 remains `{{ img.width }}` / `{{ img.height }}`, // so the fixture (stored 800x600, EXIF Orientation=6) reports 800 while the // browser renders 600. Fixing it means sourcing the dimensions from a medium // Grav has already oriented rather than the raw original — which cannot be // verified locally, since the dev container has no php-exif and so never // applies auto_fix_orientation. Left failing so the squeeze stays visible. // // 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'); // USER_DIR comes from helpers so GRAV_USER_DIR is honoured — without it a run // against a checkout detached from the served tree plants the fixture in a // different user/ than Grav renders, and LD1 fails as an opaque "card never // appeared" timeout. const { USER_DIR } = require('../helpers'); // Stored 800x600 with EXIF Orientation=6: browsers render it 600x800 portrait. const EXIF_PORTRAIT = path.join(__dirname, '../../fixtures/test-photo-exif-portrait.jpg'); 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); });