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:
@@ -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
|
||||
});
|
||||
Reference in New Issue
Block a user