2c8d676e25
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
143 lines
6.9 KiB
JavaScript
143 lines
6.9 KiB
JavaScript
// @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
|
||
});
|