// @ts-check const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const { expect } = require('@playwright/test'); // The shared photo fixture every create goes through (the post form gates submit // on at least one uploaded photo). const TEST_PHOTO = path.join(__dirname, '../fixtures/test-photo.jpg'); /** * Resolve the Grav user directory. * * Resolution order: * 1. GRAV_USER_DIR env var (set in .env or shell) * 2. Sibling `user/` directory — authoritative for this repo's layout, where * docker-compose always bind-mounts `./user` relative to the checkout. This * is correct for BOTH the main checkout and a git worktree (each worktree * serves its own `./user`), so it must be preferred over docker inspect. * 3. `docker inspect intotheeast_grav` — last-resort fallback for running the * specs detached from the served checkout. NOTE: from a worktree this points * at the MAIN checkout's container (a different `user/`), so it must never * win over the sibling dir above, or disk assertions look in the wrong tree. */ function resolveUserDir() { if (process.env.GRAV_USER_DIR) { return process.env.GRAV_USER_DIR; } const sibling = path.join(__dirname, '../../user'); if (fs.existsSync(path.join(sibling, 'config/site.yaml'))) { return sibling; } 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 sibling; } /** * Resolve the active trip slug from site.yaml `active_trip`. * * 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 tripSlug = resolveActiveTripSlug(userDir); if (!tripSlug) return null; 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 site.yaml `active_trip`. * Posted entries surface in this page's journal feed. * Falls back to '/trips/italy-2026-demo'. */ function resolveActiveTripUrl() { const slug = resolveActiveTripSlug(USER_DIR); return slug ? '/trips/' + slug : '/trips/italy-2026-demo'; } const ACTIVE_TRIP_URL = resolveActiveTripUrl(); /** * Type content into the EasyMDE editor. The underlying