lightbox-dims.spec.js hardcoded ../../../user, so a run against a checkout
detached from the served tree planted its fixture in a different user/ than
Grav renders and LD1 failed as an opaque "card never appeared" timeout.
Take USER_DIR from helpers instead, which honours GRAV_USER_DIR.
The three specs in this folder that fail do so for real, pre-existing
reasons, and both files' headers implied otherwise:
- UG1/UG2 specify a submit gate that is not implemented. post-form.js's
only create-form guard is `converting > 0` (pre-FilePond HEIC
conversion); it never inspects FilePond item state at submit time, and
.photo-convert-status is created lazily by photoStatusEl() only from the
HEIC paths — so for a plain JPEG the element never exists and both
expectations fail as "element(s) not found". UG2 is the one that matters:
a failed upload keeping its thumbnail is unguarded silent data loss.
- LD1's header described its root cause in the past tense, reading as
fixed. entry-journal.html.twig:48-49 still emits {{ img.width }} /
{{ img.height }}, so EXIF-rotated photos still declare pre-rotation dims
and PhotoSwipe still squeezes them.
Left failing rather than skipped, per retries:0 — a red test here is a real
defect, and hiding these would lose both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
4.7 KiB
JavaScript
89 lines
4.7 KiB
JavaScript
// @ts-check
|
||
// Tests: UG1–UG2 — 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).
|
||
//
|
||
// ⚠️ BOTH CASES CURRENTLY FAIL — the gate they specify is NOT implemented.
|
||
// post-form.js's only create-form submit guard is `converting > 0` (the
|
||
// pre-FilePond HEIC conversion, "Hang on — a photo is still converting."). It
|
||
// never inspects FilePond's item state at submit time. `.photo-convert-status`
|
||
// is created lazily by photoStatusEl(), which only runs from setStatus() on the
|
||
// HEIC paths — so for a plain JPEG the element never exists and both
|
||
// expectations below fail as "element(s) not found", not as a wrong message.
|
||
// refreshCollapse() does read data-filepond-item-state, but only to word the
|
||
// <summary> ("Uploading N photos…"); it gates nothing.
|
||
// These are therefore red specs describing intended behaviour. UG2 is the one
|
||
// that matters: a failed upload keeping its thumbnail is a silent-data-loss
|
||
// path with no guard. Left failing rather than skipped so the gap stays visible
|
||
// — see the plan's open items.
|
||
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();
|
||
});
|