// @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 dailies listing page, * read from the post-form.md pageconfig.parent value. * Falls back to '/trips/italy-2026-demo/dailies'. */ function resolveActiveDailiesUrl() { const postFormPath = path.join(USER_DIR, 'pages/02.post/post-form.md'); if (!fs.existsSync(postFormPath)) return '/trips/italy-2026-demo/dailies'; const content = fs.readFileSync(postFormPath, 'utf-8'); const m = content.match(/parent:\s*['"]?(\/trips\/[^'"]+\/dailies)['"]?/); return m ? m[1] : '/trips/italy-2026-demo/dailies'; } const DAILIES_URL = resolveActiveDailiesUrl(); /** * 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, DAILIES_URL };