Phase-1 fallout: delete tests for deleted pages, re-point moved-behavior tests to the trip/home page where the content now lives. - maps.spec: keep M4/M7/M8 (home+trip maps); drop M1-3,5,6,9-11 (/map, /dailies & /stories mini-maps) - map-ux: re-point MUX4/5 sort toggle to #trip-sort-toggle on trip page - nav: N1->trip page, N2->home, drop N3 (/stats) + N5 (obsolete skip) - dailies: T1->trip page; T2->trip page with oldest-first assertion; fix T3-T6 stale entry-detail selectors (.entry-* -> .journal-post-*, pre-existing failures from an earlier entry redesign) - stories: S1/S5 -> trip feed story cards ([data-type=story]) - gpx-journey: load MapUtils from the trip page instead of /map - a11y: A4 photo strips -> trip page; drop AX3 (covered by AX2) - post/helpers: verify posted entry on the trip page (ACTIVE_TRIP_URL) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
5.3 KiB
JavaScript
141 lines
5.3 KiB
JavaScript
// @ts-check
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { execSync } = require('child_process');
|
|
|
|
/**
|
|
* Resolve the Grav user directory.
|
|
*
|
|
* Resolution order:
|
|
* 1. GRAV_USER_DIR env var (set in .env or shell)
|
|
* 2. docker inspect the running intotheeast_grav container
|
|
* 3. Sibling `user/` directory (worktree fallback)
|
|
*/
|
|
function resolveUserDir() {
|
|
if (process.env.GRAV_USER_DIR) {
|
|
return process.env.GRAV_USER_DIR;
|
|
}
|
|
try {
|
|
const raw = execSync(
|
|
"docker inspect intotheeast_grav --format '{{range .Mounts}}{{if eq .Destination \"/var/www/html/user\"}}{{.Source}}{{end}}{{end}}'",
|
|
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
).trim();
|
|
if (raw) return raw;
|
|
} catch (_) {
|
|
// docker not available or container not running
|
|
}
|
|
return path.join(__dirname, '../../user');
|
|
}
|
|
|
|
/**
|
|
* Resolve the active dailies directory from the post-form.md pageconfig.
|
|
*
|
|
* 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.
|
|
*/
|
|
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 tripsBase = path.join(userDir, 'pages/01.trips');
|
|
if (!fs.existsSync(tripsBase)) return null;
|
|
|
|
const tripFolder = fs.readdirSync(tripsBase).find(f => f === tripSlug || f.endsWith('.' + tripSlug) || f.includes(tripSlug));
|
|
if (!tripFolder) return null;
|
|
|
|
const dailiesBase = path.join(tripsBase, tripFolder);
|
|
const dailiesFolder = fs.readdirSync(dailiesBase).find(f => f === 'dailies' || f === '01.dailies' || f.endsWith('.dailies'));
|
|
if (!dailiesFolder) return null;
|
|
|
|
return path.join(dailiesBase, dailiesFolder);
|
|
}
|
|
|
|
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.
|
|
* 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 ACTIVE_TRIP_URL = resolveActiveTripUrl();
|
|
|
|
/**
|
|
* Wait for all filepond items to finish XHR upload.
|
|
*/
|
|
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 });
|
|
}
|
|
|
|
/**
|
|
* Submit the post form with minimal required fields and return the unique title marker.
|
|
* Caller is responsible for cleanup via cleanupEntry().
|
|
*/
|
|
async function postEntry(page, { titleTag, content = 'Automated test. Safe to delete.', city = '', country = '' } = {}) {
|
|
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);
|
|
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());
|
|
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
|
|
return titleTag;
|
|
}
|
|
|
|
/**
|
|
* Find a tracker entry folder by a unique slug fragment, then delete it.
|
|
*/
|
|
function cleanupEntry(slugFragment) {
|
|
if (!slugFragment) return;
|
|
if (!fs.existsSync(TRACKER_DIR)) return;
|
|
const entries = fs.readdirSync(TRACKER_DIR);
|
|
const match = entries.find(e => e.includes(slugFragment));
|
|
if (match) {
|
|
fs.rmSync(path.join(TRACKER_DIR, match), { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the first entry folder matching a slug fragment and return its full path.
|
|
*/
|
|
function findEntry(slugFragment) {
|
|
if (!fs.existsSync(TRACKER_DIR)) return null;
|
|
const entries = fs.readdirSync(TRACKER_DIR);
|
|
const match = entries.find(e => e.includes(slugFragment));
|
|
return match ? path.join(TRACKER_DIR, match) : null;
|
|
}
|
|
|
|
/**
|
|
* Read the entry .md file (entry.md or entry.en.md) from an entry folder.
|
|
*/
|
|
function readEntryMd(entryDir) {
|
|
const name = ['entry.md', 'entry.en.md'].find(f => fs.existsSync(path.join(entryDir, f)));
|
|
if (!name) return null;
|
|
return fs.readFileSync(path.join(entryDir, name), 'utf-8');
|
|
}
|
|
|
|
module.exports = { waitForFilePondUpload, postEntry, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, ACTIVE_TRIP_URL };
|