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:
+46
-31
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user