test(post-form): retarget suite to redesigned /post + add UX coverage (U7)

- test-form-config.sh: assert parent is NOT hardcoded (injected server-side),
  active_trip set in site.yaml, and the new fields incl. custom 'photos' type.
- helpers.js: resolve active trip from site.active_trip (parent coupling gone);
  fillEditor() drives EasyMDE via window.postFormEditor; waitForPhotoUpload()
  waits on the new picker.
- post.spec / validation.spec: content via the editor, filepond selectors ->
  the photo picker, P8 checks editor value, V3/V4 exercise the picker cap +
  fail-closed non-image.
- post-form-ux.spec.js (new): AE3 disclosure, AE1 HEIC->JPEG, AE4 corrupt-HEIC
  fail-closed, R18 weather gating, R20 draft restore.
- fixtures: real + corrupt .heic.
- test-post.sh: resolve dailies dir from active_trip.

Refs AE1-AE4, R18, R20, U7.
This commit is contained in:
2026-07-04 16:56:07 +02:00
parent 1710ad8612
commit 421c21345e
8 changed files with 226 additions and 90 deletions
+20 -31
View File
@@ -2,6 +2,7 @@
// Tests: V1V4 — form validation and input constraints
const { test, expect } = require('@playwright/test');
const path = require('path');
const { fillEditor, waitForPhotoUpload } = require('../helpers');
const TEST_PHOTO = path.join(__dirname, '../../fixtures/test-photo.jpg');
const TEST_NONIMAGE = path.join(__dirname, '../../fixtures/test-nonimage.txt');
@@ -10,7 +11,7 @@ const TEST_NONIMAGE = path.join(__dirname, '../../fixtures/test-nonimage.txt');
test('V1: submit without title shows a validation error or stays on /post', async ({ page }) => {
await page.goto('/post');
// Leave title empty, fill only content
await page.fill('textarea[name="data[content]"]', 'Content without a title.');
await fillEditor(page, 'Content without a title.');
await page.locator('.btn-post').evaluate(el => el.click());
// Grav either shows an error message OR re-renders the form (stays on /post).
@@ -24,7 +25,7 @@ test('V1: submit without title shows a validation error or stays on /post', asyn
test('V2: submit without content shows a validation error or stays on /post', async ({ page }) => {
await page.goto('/post');
await page.fill('input[name="data[title]"]', 'V2 title no content');
// Leave content (textarea) empty
// Leave content (editor) empty
await page.locator('.btn-post').evaluate(el => el.click());
await page.waitForTimeout(2_000);
@@ -33,43 +34,31 @@ test('V2: submit without content shows a validation error or stays on /post', as
});
// ── V3: Photo limit (max 4) ───────────────────────────────────────────────────
test('V3: filepond rejects a 5th photo when limit is 4', async ({ page }) => {
test('V3: photo picker caps attachments at 4', async ({ page }) => {
await page.goto('/post');
const input = page.locator('input.photo-picker__input');
// Upload 4 photos (all the same fixture — we just need 4 items)
const fourPhotos = [TEST_PHOTO, TEST_PHOTO, TEST_PHOTO, TEST_PHOTO];
await page.locator('input.filepond--browser').setInputFiles(fourPhotos);
// Attach 4 photos (same fixture — we only need four items).
await input.setInputFiles([TEST_PHOTO, TEST_PHOTO, TEST_PHOTO, TEST_PHOTO]);
await waitForPhotoUpload(page, 4);
// Wait for all 4 items to appear
await page.waitForFunction(() =>
document.querySelectorAll('.filepond--item').length === 4,
{ timeout: 10_000 }
);
// Attempt a 5th — filepond should ignore it once the limit is reached
await page.locator('input.filepond--browser').setInputFiles([TEST_PHOTO]);
// A 5th is rejected once the limit is reached — no new card, and a hint shows.
await input.setInputFiles([TEST_PHOTO]);
await page.waitForTimeout(500);
const itemCount = await page.locator('.filepond--item').count();
expect(itemCount).toBe(4);
await expect(page.locator('.photo-card')).toHaveCount(4);
await expect(page.locator('.photo-picker__hint')).toContainText(/up to 4/i);
});
// ── V4: Non-image file rejected ───────────────────────────────────────────────
test('V4: filepond rejects non-image files', async ({ page }) => {
// ── V4: Non-image file rejected (fail-closed) ─────────────────────────────────
test('V4: a non-image upload fails closed and is not attached', async ({ page }) => {
await page.goto('/post');
await page.locator('input.filepond--browser').setInputFiles(TEST_NONIMAGE);
await page.waitForTimeout(1_000);
// The picker accepts anything the OS dialog allows; the server's uploadFiles
// accept-check (image/*) rejects a .txt, so the card ends in the error state
// and never reaches "done" — the original is never posted.
await page.locator('input.photo-picker__input').setInputFiles(TEST_NONIMAGE);
const items = page.locator('.filepond--item');
const count = await items.count();
if (count > 0) {
// If filepond added it, it must show an error state — not processing-complete
const state = await items.first().getAttribute('data-filepond-item-state');
expect(state).not.toBe('processing-complete');
} else {
// Silently rejected before adding — also a pass
expect(count).toBe(0);
}
await expect(page.locator('.photo-card.is-error')).toBeVisible({ timeout: 15_000 });
await expect(page.locator('.photo-card.is-done')).toHaveCount(0);
});