feat: add comprehensive Playwright UI test suite

25 tests across auth (A1-A5), posting (P1-P5), validation (V1-V4),
tracker (T1-T5), and nav (N1-N5). Uses storageState for single login
per run. Replaces post-with-photo.spec.js with post.spec.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:34:11 +02:00
parent 545e3f5ba0
commit a50e7f5386
12 changed files with 567 additions and 91 deletions
+138
View File
@@ -0,0 +1,138 @@
// @ts-check
// Tests: P1P5 — form submission happy paths
// Replaces post-with-photo.spec.js
const { test, expect } = require('@playwright/test');
const path = require('path');
const fs = require('fs');
const { waitForFilePondUpload, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR } = require('./helpers');
const TEST_PHOTO = path.join(__dirname, '../fixtures/test-photo.jpg');
// Track slugs created per test for cleanup
const created = [];
test.afterAll(() => {
created.forEach(cleanupEntry);
});
// ── P1: Post without photo ─────────────────────────────────────────────────────
test('P1: post text-only entry → created on disk and visible on /tracker', async ({ page }) => {
const tag = `p1-${Date.now()}`;
const title = `UI Test ${tag}`;
await page.goto('/post');
await page.fill('input[name="data[title]"]', title);
await page.fill('textarea[name="data[content]"]', 'Text-only test entry. Safe to delete.');
await page.fill('input[name="data[location_city]"]', 'Testville');
await page.fill('input[name="data[location_country]"]', 'Testland');
await page.locator('.btn-post').evaluate(el => el.click());
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
const entryDir = findEntry(tag);
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
created.push(tag);
const md = readEntryMd(entryDir);
expect(md).toContain(tag);
// No photo expected
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
expect(photos.length, 'Text-only entry should have no photos').toBe(0);
await page.goto('/tracker');
await expect(page.locator('body')).toContainText(tag);
});
// ── P2: Post with photo ────────────────────────────────────────────────────────
test('P2: post entry with photo → photo saved in entry folder and visible on /tracker', async ({ page }) => {
const tag = `p2-${Date.now()}`;
const title = `UI Test ${tag}`;
await page.goto('/post');
await page.fill('input[name="data[title]"]', title);
await page.fill('textarea[name="data[content]"]', 'Photo test entry. Safe to delete.');
await page.fill('input[name="data[location_city]"]', 'Testville');
await page.fill('input[name="data[location_country]"]', 'Testland');
await page.locator('input.filepond--browser').setInputFiles(TEST_PHOTO);
await waitForFilePondUpload(page);
await page.locator('.btn-post').evaluate(el => el.click());
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
const entryDir = findEntry(tag);
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
created.push(tag);
const md = readEntryMd(entryDir);
expect(md).toContain(tag);
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
expect(photos.length, 'At least one photo should be saved').toBeGreaterThan(0);
await page.goto('/tracker');
await expect(page.locator('body')).toContainText(tag);
});
// ── P3: Post with city + country ──────────────────────────────────────────────
test('P3: post entry with city/country → frontmatter contains location', async ({ page }) => {
const tag = `p3-${Date.now()}`;
const title = `UI Test ${tag}`;
await page.goto('/post');
await page.fill('input[name="data[title]"]', title);
await page.fill('textarea[name="data[content]"]', 'Location test. Safe to delete.');
await page.fill('input[name="data[location_city]"]', 'Kyoto');
await page.fill('input[name="data[location_country]"]', 'Japan');
await page.locator('.btn-post').evaluate(el => el.click());
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
const entryDir = findEntry(tag);
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
created.push(tag);
const md = readEntryMd(entryDir);
expect(md).toContain('Kyoto');
expect(md).toContain('Japan');
});
// ── P4: Post with lat/lng ─────────────────────────────────────────────────────
test('P4: post entry with lat/lng → coordinates saved in frontmatter', async ({ page }) => {
const tag = `p4-${Date.now()}`;
const title = `UI Test ${tag}`;
await page.goto('/post');
await page.fill('input[name="data[title]"]', title);
await page.fill('textarea[name="data[content]"]', 'GPS test. Safe to delete.');
// lat/lng fields are CSS-hidden (designed to be filled by the Get Location button);
// set values directly via JS to simulate what the button would do.
await page.evaluate(() => {
document.querySelector('input[name="data[lat]"]').value = '35.6762';
document.querySelector('input[name="data[lng]"]').value = '139.6503';
});
await page.locator('.btn-post').evaluate(el => el.click());
await page.waitForSelector('.form-messages, .notices', { timeout: 15_000 });
const entryDir = findEntry(tag);
expect(entryDir, 'Entry folder should exist on disk').toBeTruthy();
created.push(tag);
const md = readEntryMd(entryDir);
expect(md).toContain('35.6762');
expect(md).toContain('139.6503');
});
// ── P5: "Get Location" button fills lat/lng (TDD — fails until feature built) ──
test('P5: Get Location button fills lat/lng from browser geolocation', async ({ page, context }) => {
await context.grantPermissions(['geolocation']);
await context.setGeolocation({ latitude: 35.6762, longitude: 139.6503 });
await page.goto('/post');
await page.click('#get-location');
// Allow a moment for the geolocation callback to fire
await page.waitForTimeout(500);
await expect(page.locator('input[name="data[lat]"]')).toHaveValue(/35\.67/);
await expect(page.locator('input[name="data[lng]"]')).toHaveValue(/139\.65/);
});