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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
This commit is contained in:
2026-06-21 16:35:48 +02:00
parent 2ab0b13eb6
commit 2c8d676e25
2 changed files with 143 additions and 1 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ const fs = require('fs');
const USER = process.env.GRAV_TEST_USER; const USER = process.env.GRAV_TEST_USER;
const PASS = process.env.GRAV_TEST_PASS; 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 }) => { setup('authenticate', async ({ page }) => {
if (!USER || !PASS) throw new Error('GRAV_TEST_USER and GRAV_TEST_PASS must be set in .env'); if (!USER || !PASS) throw new Error('GRAV_TEST_USER and GRAV_TEST_PASS must be set in .env');
+142
View File
@@ -0,0 +1,142 @@
// @ts-check
// Tests: GM1GM7 — 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
});