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
+46 -31
View File
@@ -28,22 +28,31 @@ function resolveUserDir() {
}
/**
* Resolve the active dailies directory from the post-form.md pageconfig.
* Resolve the active trip slug from site.yaml `active_trip`.
*
* The post form stores `pageconfig.parent` as a Grav route such as
* `/trips/italy-2026-demo/dailies`. We map that to the filesystem by
* scanning for a folder whose name ends with the trip slug.
* The post form no longer hardcodes `pageconfig.parent` — the write target is
* injected server-side from `site.active_trip` (see the cache-on-save plugin).
* `active_trip` is a full route ("/trips/italy-2026-demo") or a bare slug; both
* reduce to the trip slug here.
*/
function resolveActiveTripSlug(userDir) {
const sitePath = path.join(userDir, 'config/site.yaml');
if (!fs.existsSync(sitePath)) return null;
const content = fs.readFileSync(sitePath, 'utf-8');
const m = content.match(/^active_trip:\s*['"]?(\S+?)['"]?\s*$/m);
if (!m) return null;
return m[1]
.replace(/^\/?trips\//, '') // strip a leading /trips/
.replace(/^\//, '')
.replace(/\/.*$/, ''); // keep only the slug segment
}
/**
* Resolve the active dailies directory on disk from the active trip slug.
*/
function resolveDailiesDir(userDir) {
const postFormPath = path.join(userDir, 'pages/02.post/post-form.md');
if (!fs.existsSync(postFormPath)) {
// fallback: search all trips for a dailies dir
return null;
}
const content = fs.readFileSync(postFormPath, 'utf-8');
const m = content.match(/parent:\s*['"]?\/trips\/([^/'"]+)\/dailies/);
if (!m) return null;
const tripSlug = m[1];
const tripSlug = resolveActiveTripSlug(userDir);
if (!tripSlug) return null;
const tripsBase = path.join(userDir, 'pages/01.trips');
if (!fs.existsSync(tripsBase)) return null;
@@ -62,31 +71,37 @@ const USER_DIR = resolveUserDir();
const TRACKER_DIR = resolveDailiesDir(USER_DIR) || path.join(USER_DIR, 'pages/01.trips/italy-2026-demo/01.dailies');
/**
* The Grav route to the active trip page, derived from the post-form.md
* pageconfig.parent value (the dailies container route, minus the trailing
* `/dailies`). Posted entries surface in this page's journal feed.
* The Grav route to the active trip page, derived from site.yaml `active_trip`.
* Posted entries surface in this page's journal feed.
* Falls back to '/trips/italy-2026-demo'.
*/
function resolveActiveTripUrl() {
const postFormPath = path.join(USER_DIR, 'pages/02.post/post-form.md');
if (!fs.existsSync(postFormPath)) return '/trips/italy-2026-demo';
const content = fs.readFileSync(postFormPath, 'utf-8');
const m = content.match(/parent:\s*['"]?(\/trips\/[^'"]+)\/dailies['"]?/);
return m ? m[1] : '/trips/italy-2026-demo';
const slug = resolveActiveTripSlug(USER_DIR);
return slug ? '/trips/' + slug : '/trips/italy-2026-demo';
}
const ACTIVE_TRIP_URL = resolveActiveTripUrl();
/**
* Wait for all filepond items to finish XHR upload.
* Type content into the EasyMDE editor. The underlying <textarea> is hidden by
* EasyMDE, so we set the value through the instance the bundle exposes on
* window.postFormEditor (which also syncs the textarea for submission).
*/
async function waitForFilePondUpload(page) {
await page.waitForFunction(() => {
const items = document.querySelectorAll('.filepond--item[data-filepond-item-state]');
return items.length > 0 && [...items].every(
el => el.getAttribute('data-filepond-item-state') === 'processing-complete'
);
}, { timeout: 20_000 });
async function fillEditor(page, text) {
await page.waitForFunction(() => window.postFormEditor != null, { timeout: 10_000 });
await page.evaluate((t) => window.postFormEditor.value(t), text);
}
/**
* Wait for the custom photo picker to finish converting + uploading photos
* (each successfully attached photo gets a `.photo-card.is-done`).
*/
async function waitForPhotoUpload(page, count = 1) {
await page.waitForFunction(
(n) => document.querySelectorAll('.photo-card.is-done').length >= n,
count,
{ timeout: 30_000 }
);
}
/**
@@ -97,7 +112,7 @@ async function postEntry(page, { titleTag, content = 'Automated test. Safe to de
const title = `UI Test ${titleTag} ${Date.now()}`;
await page.goto('/post');
await page.fill('input[name="data[title]"]', title);
await page.fill('textarea[name="data[content]"]', content);
await fillEditor(page, content);
if (city) await page.fill('input[name="data[location_city]"]', city);
if (country) await page.fill('input[name="data[location_country]"]', country);
await page.locator('.btn-post').evaluate(el => el.click());
@@ -137,4 +152,4 @@ function readEntryMd(entryDir) {
return fs.readFileSync(path.join(entryDir, name), 'utf-8');
}
module.exports = { waitForFilePondUpload, postEntry, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, ACTIVE_TRIP_URL };
module.exports = { fillEditor, waitForPhotoUpload, postEntry, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, ACTIVE_TRIP_URL };