From 2c8d676e257679c2abe2fbdc8a69ccd2f273226c Mon Sep 17 00:00:00 2001 From: Mischa Date: Sun, 21 Jun 2026 16:35:48 +0200 Subject: [PATCH] test: add GPX Manager end-to-end spec (GM1-GM7) Also fix auth.setup.js AUTH_FILE path: the file lives in tests/ui/auth/ so the relative path to tests/.auth/user.json needs ../../ not ../ to match the storageState path in playwright.config.js. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM --- tests/ui/auth/auth.setup.js | 2 +- tests/ui/gpx/gpx-manager.spec.js | 142 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 tests/ui/gpx/gpx-manager.spec.js diff --git a/tests/ui/auth/auth.setup.js b/tests/ui/auth/auth.setup.js index f0fc9a3..84520af 100644 --- a/tests/ui/auth/auth.setup.js +++ b/tests/ui/auth/auth.setup.js @@ -5,7 +5,7 @@ const fs = require('fs'); const USER = process.env.GRAV_TEST_USER; const PASS = process.env.GRAV_TEST_PASS; -const AUTH_FILE = path.join(__dirname, '../.auth/user.json'); +const AUTH_FILE = path.join(__dirname, '../../.auth/user.json'); setup('authenticate', async ({ page }) => { if (!USER || !PASS) throw new Error('GRAV_TEST_USER and GRAV_TEST_PASS must be set in .env'); diff --git a/tests/ui/gpx/gpx-manager.spec.js b/tests/ui/gpx/gpx-manager.spec.js new file mode 100644 index 0000000..2236d90 --- /dev/null +++ b/tests/ui/gpx/gpx-manager.spec.js @@ -0,0 +1,142 @@ +// @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 +});