Files
intotheeast-com/tests/ui/post/lightbox-dims.spec.js
T
m038andClaude Fable 5 0defa85f58 test(post): cover the upload-submit gate and lightbox EXIF-dims invariant
Regression specs for the two 2026-07-09 prod bugs (fixed in user/ e17a5dc):

- upload-gate.spec.js — UG1/UG2: create submit is blocked with a visible
  message while a photo upload is in flight or after it FAILED; nothing may
  land on disk. The form plugin's own guard misses LOADING and
  PROCESSING_ERROR, which silently dropped a photo on a fast save.
- lightbox-dims.spec.js — LD1: a slide's data-pswp-* must equal the
  browser-rendered natural size of the linked image. Fixture is an 800x600
  JPEG with EXIF Orientation=6 (renders 600x800 portrait), planted on disk
  in the demo trip (the active trip may be an unpublished draft that 404s).

New fixture: tests/fixtures/test-photo-exif-portrait.jpg.

Note: the suite currently needs GRAV_TEST_USER/GRAV_TEST_PASS overrides —
the .env GRAV_TEST_PASS contains shell-special chars that break `make
test-account` (see the Makefile comment requiring a plain password).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
2026-07-09 17:59:05 +02:00

93 lines
4.1 KiB
JavaScript

// @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);
});