// @ts-check // Tests: P1–P5 — 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, DAILIES_URL } = 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 /dailies', 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(DAILIES_URL); await expect(page.locator('body')).toContainText(tag); }); // ── P2: Post with photo ──────────────────────────────────────────────────────── test.skip('P2: post entry with photo → photo saved in entry folder and visible on /dailies', 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(DAILIES_URL); 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/); }); // ── P6: Success message is visible after submit ─────────────────────────────── test('P6: successful submit shows "Entry posted successfully!" message', async ({ page }) => { const tag = `p6-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await page.fill('textarea[name="data[content]"]', 'P6 test. Safe to delete.'); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.form-messages, .notices')).toContainText( 'Entry posted successfully!', { timeout: 15_000 } ); created.push(tag); }); // ── P7: Entry is saved with a recent timestamp (default: now resolved server-side) ── // Note: the form renders `default: now` as the literal string "now" in the HTML input. // The server resolves it to the current timestamp when processing the submission. // This test verifies that server-side behaviour. test('P7: submitted entry is saved with a date within 5 minutes of now', async ({ page }) => { const tag = `p7-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await page.fill('textarea[name="data[content]"]', 'P7 date test. Safe to delete.'); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.form-messages, .notices')).toContainText( 'Entry posted successfully!', { timeout: 15_000 } ); created.push(tag); const entryDir = findEntry(tag); expect(entryDir, 'Entry folder should exist on disk').toBeTruthy(); const md = readEntryMd(entryDir); expect(md, 'Entry markdown should be readable').toBeTruthy(); // Extract the date frontmatter value — format: "YYYY-MM-DD HH:mm" const dateMatch = md.match(/^date:\s*['"]?(\d{4}-\d{2}-\d{2} \d{2}:\d{2})['"]?/m); expect(dateMatch, 'Frontmatter must contain a date field').toBeTruthy(); const parsed = new Date(dateMatch[1].replace(' ', 'T')); expect(isNaN(parsed.getTime()), 'Saved date must parse as a valid date').toBe(false); const diffMs = Math.abs(Date.now() - parsed.getTime()); expect(diffMs, 'Saved date must be within 5 minutes of test run').toBeLessThan(5 * 60 * 1000); }); // ── P8: Form fields are cleared after successful submit (reset: true) ───────── test('P8: title and content fields are empty after a successful submit', async ({ page }) => { const tag = `p8-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await page.fill('textarea[name="data[content]"]', 'P8 reset test. Safe to delete.'); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.form-messages, .notices')).toContainText( 'Entry posted successfully!', { timeout: 15_000 } ); // After reset, the form fields should be empty await expect(page.locator('input[name="data[title]"]')).toHaveValue(''); await expect(page.locator('textarea[name="data[content]"]')).toHaveValue(''); created.push(tag); });