// @ts-check // Tests: GM1–GM7 — GPX Manager end-to-end (real API calls) // Requires: Grav server at localhost:8081, demo-load completed, user logged in. const { test, expect } = require('@playwright/test'); const fs = require('fs'); const path = require('path'); const BASE_URL = process.env.GRAV_BASE_URL || 'http://localhost:8081'; const API = `${BASE_URL}/api/v1`; const TRIP_ROUTE = '/trips/italy-2026-demo'; const AUTH_FILE = path.join(__dirname, '../../.auth/user.json'); const GPX_FIXTURE = path.join(__dirname, '../../fixtures/test-route.gpx'); const GPX_FIXTURE_CONTENT = fs.readFileSync(GPX_FIXTURE); // Track uploaded filenames for cleanup const uploaded = []; async function apiDelete(filename) { const authState = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8')); const cookie = authState.cookies .map(c => `${c.name}=${c.value}`) .join('; '); await fetch( `${API}/pages${TRIP_ROUTE}/media/${encodeURIComponent(filename)}`, { method: 'DELETE', headers: { Cookie: cookie } } ); } test.afterAll(async () => { for (const name of uploaded) { try { await apiDelete(name); } catch (_) { /* best-effort */ } } }); // ── GM1: Page loads with auth ───────────────────────────────────────────────── test('GM1: /gpx-manager loads with auth and shows one section per trip', async ({ page }) => { await page.goto('/gpx-manager'); const sections = page.locator('.gpx-trip'); await expect(sections.first()).toBeVisible({ timeout: 8000 }); const count = await sections.count(); expect(count, 'At least one trip section').toBeGreaterThan(0); }); // ── GM2: Page without auth shows login form ─────────────────────────────────── test('GM2: /gpx-manager without auth renders inline login form', async ({ browser }) => { const ctx = await browser.newContext({ storageState: { cookies: [], origins: [] } }); const page = await ctx.newPage(); await page.goto(`${BASE_URL}/gpx-manager`); await expect(page.locator('#grav-login')).toBeVisible({ timeout: 8000 }); await ctx.close(); }); // ── GM3: File list settles (loading placeholder gone) ──────────────────────── test('GM3: file list resolves — loading placeholder is gone after API call', async ({ page }) => { await page.goto('/gpx-manager'); const italySection = page.locator('.gpx-trip[data-route="/trips/italy-2026-demo"]'); await expect(italySection).toBeVisible({ timeout: 8000 }); // Wait for loading placeholder to disappear await expect(italySection.locator('.gpx-loading')).toHaveCount(0, { timeout: 15000 }); }); // ── GM4: Upload test-route.gpx → appears in file list ──────────────────────── test('GM4: uploading test-route.gpx shows it in the file list', async ({ page }) => { await page.goto('/gpx-manager'); const italySection = page.locator('.gpx-trip[data-route="/trips/italy-2026-demo"]'); await expect(italySection).toBeVisible({ timeout: 8000 }); await expect(italySection.locator('.gpx-loading')).toHaveCount(0, { timeout: 15000 }); const form = italySection.locator('.gpx-upload-form'); await form.locator('input[type=file]').setInputFiles({ name: 'test-route.gpx', mimeType: 'application/gpx+xml', buffer: GPX_FIXTURE_CONTENT, }); await form.locator('.gpx-upload-btn').click(); // Wait for status to show "Uploaded!" and file list to refresh await expect(form.locator('.gpx-status')).toContainText('Uploaded!', { timeout: 15000 }); await expect(italySection.locator('.gpx-table td', { hasText: 'test-route.gpx' })).toBeVisible({ timeout: 10000 }); uploaded.push('test-route.gpx'); }); // ── GM5: Filename with spaces/caps gets slugified ───────────────────────────── test('GM5: filename with spaces and capitals is slugified before upload', async ({ page }) => { await page.goto('/gpx-manager'); const italySection = page.locator('.gpx-trip[data-route="/trips/italy-2026-demo"]'); await expect(italySection).toBeVisible({ timeout: 8000 }); await expect(italySection.locator('.gpx-loading')).toHaveCount(0, { timeout: 15000 }); const form = italySection.locator('.gpx-upload-form'); await form.locator('input[type=file]').setInputFiles({ name: 'My Route 1.gpx', mimeType: 'application/gpx+xml', buffer: GPX_FIXTURE_CONTENT, }); await form.locator('.gpx-upload-btn').click(); await expect(form.locator('.gpx-status')).toContainText('Uploaded!', { timeout: 15000 }); // The client-side slugify turns "My Route 1.gpx" → "my-route-1.gpx" await expect(italySection.locator('.gpx-table td', { hasText: 'my-route-1.gpx' })).toBeVisible({ timeout: 10000 }); uploaded.push('my-route-1.gpx'); }); // ── GM6: Submit without file shows error message ────────────────────────────── test('GM6: submitting upload form without a file shows "Choose a file first."', async ({ page }) => { await page.goto('/gpx-manager'); const italySection = page.locator('.gpx-trip[data-route="/trips/italy-2026-demo"]'); await expect(italySection).toBeVisible({ timeout: 8000 }); const form = italySection.locator('.gpx-upload-form'); await form.locator('.gpx-upload-btn').click(); await expect(form.locator('.gpx-status')).toHaveText('Choose a file first.'); }); // ── GM7: Delete uploaded file removes it from list ─────────────────────────── test('GM7: deleting an uploaded file removes its row from the file list', async ({ page }) => { // Upload a file first so we have something to delete await page.goto('/gpx-manager'); const italySection = page.locator('.gpx-trip[data-route="/trips/italy-2026-demo"]'); await expect(italySection).toBeVisible({ timeout: 8000 }); await expect(italySection.locator('.gpx-loading')).toHaveCount(0, { timeout: 15000 }); const form = italySection.locator('.gpx-upload-form'); await form.locator('input[type=file]').setInputFiles({ name: 'to-delete.gpx', mimeType: 'application/gpx+xml', buffer: GPX_FIXTURE_CONTENT, }); await form.locator('.gpx-upload-btn').click(); await expect(form.locator('.gpx-status')).toContainText('Uploaded!', { timeout: 15000 }); // Click delete for the uploaded file page.once('dialog', dialog => dialog.accept()); await italySection.locator('.gpx-delete[data-filename="to-delete.gpx"]').click(); // Row must disappear await expect(italySection.locator('.gpx-table td', { hasText: 'to-delete.gpx' })) .toHaveCount(0, { timeout: 10000 }); // No cleanup needed — the test deleted it itself });