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
This commit is contained in:
2026-07-09 17:59:05 +02:00
co-authored by Claude Fable 5
parent 9ffeb4d2d8
commit 0defa85f58
3 changed files with 166 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
// @ts-check
// Tests: UG1UG2 — the create form must never submit while a photo is not
// fully uploaded (BUG 2026-07-09: a fast save after adding a picture posted a
// text-only entry; the photo was silently dropped).
//
// The form plugin's own submit guard (filepond-handler.js) only blocks the
// PROCESSING / PROCESSING_QUEUED states. Two states slip through it:
// - UG1: LOADING — the moment between picking a file and it entering the
// upload queue (the "too quick" click). Guarded here with a slowed upload.
// - UG2: PROCESSING_ERROR — a failed upload keeps its thumbnail, passes the
// ≥1-photo validation, and the form posts without the file. This is the
// silent-data-loss path.
// post-form.js owns the complete gate (theme code; the form plugin is
// GPM-managed and not patchable in-repo).
const { test, expect } = require('@playwright/test');
const { fillEditor, findEntry, cleanupEntry, TEST_PHOTO } = require('../helpers');
// FilePond uploads go to the form route with .json + the file-upload task
// (Form.php:1183: withExtension('json')->withGravParam('task','file-upload')),
// i.e. /post.json/task:file-upload — the task is a PATH segment, so a glob
// with a non-slash-crossing `*` misses it; match by regex instead.
const UPLOAD_URL = /\/post\.json\//;
const created = [];
test.afterAll(() => created.forEach(cleanupEntry));
async function fillCreateForm(page, tag) {
await page.goto('/post');
await page.fill('input[name="data[title]"]', `UI Test ${tag}`);
await fillEditor(page, `Upload-gate fixture ${tag}. Safe to delete.`);
}
// ── UG1: submit while the upload is still in flight is blocked ────────────────
test('UG1: submitting while a photo upload is in flight is blocked with a message', async ({ page }) => {
const tag = `ug1-${Date.now()}`;
// Slow the upload down so the submit click lands mid-flight.
await page.route(UPLOAD_URL, async (route) => {
await new Promise((r) => setTimeout(r, 6000));
await route.continue();
});
await fillCreateForm(page, tag);
await page.locator('input.filepond--browser').setInputFiles(TEST_PHOTO);
// The item exists but cannot have finished uploading (route is held).
await page.waitForSelector('.filepond--item');
await page.locator('.btn-post').evaluate((el) => el.click());
created.push(tag);
// Blocked: visible feedback, no success notice, nothing written to disk.
await expect(page.locator('.photo-convert-status')).toContainText(/uploading/i);
await expect(page.locator('.notices.success')).toHaveCount(0);
expect(findEntry(tag), 'no entry may be created mid-upload').toBeNull();
});
// ── UG2: submit with a FAILED upload is blocked, not silently posted ──────────
test('UG2: submitting after a photo upload failed is blocked with an error', async ({ page }) => {
const tag = `ug2-${Date.now()}`;
// Make the upload fail server-side (transient network/limit failure).
await page.route(UPLOAD_URL, (route) => route.fulfill({ status: 500, body: 'nope' }));
await fillCreateForm(page, tag);
await page.locator('input.filepond--browser').setInputFiles(TEST_PHOTO);
// Wait for FilePond to mark the item as failed.
await page.waitForSelector('.filepond--item[data-filepond-item-state*="error"]', { timeout: 20_000 });
await page.locator('.btn-post').evaluate((el) => el.click());
created.push(tag);
// Blocked: the error is surfaced, the form did not post, no disk write.
await expect(page.locator('.photo-convert-status')).toContainText(/failed/i);
await expect(page.locator('.notices.success')).toHaveCount(0);
expect(findEntry(tag), 'a failed upload must never produce a photo-less entry').toBeNull();
});