// @ts-check // Tests: post-form redesign UX — disclosure, HEIC conversion + failure, // weather-button gating, draft restore. Covers AE1, AE3, AE4 and R18/R20. const { test, expect } = require('@playwright/test'); const path = require('path'); const fs = require('fs'); const { fillEditor, waitForPhotoUpload, cleanupEntry, findEntry } = require('../helpers'); const TEST_HEIC = path.join(__dirname, '../../fixtures/test-photo.heic'); const TEST_CORRUPT_HEIC = path.join(__dirname, '../../fixtures/test-corrupt.heic'); const TEST_JPG = path.join(__dirname, '../../fixtures/test-photo.jpg'); const TEST_JPG_B = path.join(__dirname, '../../fixtures/test-photo-b.jpg'); const DRAFT_KEY = 'intotheeast:new-entry-draft'; const created = []; test.afterAll(() => { created.forEach(cleanupEntry); }); // ── AE3: advanced fields sit behind "More options" ──────────────────────────── test('AE3: advanced fields are hidden until "More options" is expanded', async ({ page }) => { await page.goto('/post'); const hero = page.locator('input[name="data[hero_image]"]'); await expect(page.locator('details.more-options')).toBeAttached(); await expect(hero).toBeHidden(); // collapsed
hides content await page.locator('.more-options__summary').click(); await expect(hero).toBeVisible(); }); // ── AE1: HEIC → JPEG conversion, posted with a working thumbnail ─────────────── test('AE1: a HEIC photo is converted to JPEG client-side and posted', async ({ page }) => { const tag = `heic-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await fillEditor(page, 'HEIC conversion test. Safe to delete.'); await page.locator('input.filepond--browser').setInputFiles(TEST_HEIC); await waitForPhotoUpload(page, 1); // converted (beforeAddFile) + uploaded via FilePond // The photo section auto-collapses to a summary bar once the upload settles. await expect(page.locator('.photos-collapse__summary')).toContainText('1 photo ready'); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.notices')).toContainText('Entry posted successfully!', { timeout: 15_000 }); created.push(tag); const dir = findEntry(tag); expect(dir, 'Entry folder should exist on disk').toBeTruthy(); const files = fs.readdirSync(dir); expect(files.some(f => /\.jpe?g$/i.test(f)), 'a JPEG should be posted').toBe(true); expect(files.some(f => /\.heic$/i.test(f)), 'the original HEIC must NOT be posted').toBe(false); }); // ── AE4: corrupt HEIC fails closed; Submit + other photos stay usable ───────── test('AE4: a corrupt HEIC is blocked (fail-closed) and never posted', async ({ page }) => { const tag = `heicfail-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await fillEditor(page, 'HEIC failure test. Safe to delete.'); await page.locator('input.filepond--browser').setInputFiles(TEST_CORRUPT_HEIC); // Conversion fails → inline error status, original HEIC never added to FilePond. await expect(page.locator('.photo-convert-status.form-status--err')).toBeVisible({ timeout: 15_000 }); await expect(page.locator('.btn-post')).toBeEnabled(); // Submit still usable await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.notices')).toContainText('Entry posted successfully!', { timeout: 15_000 }); created.push(tag); const dir = findEntry(tag); expect(dir).toBeTruthy(); const files = fs.readdirSync(dir); expect(files.some(f => /\.heic$/i.test(f)), 'the corrupt HEIC must never be posted').toBe(false); }); // ── Photo section collapses to a summary after upload, re-expands on tap ────── test('photo section auto-collapses to a summary after upload and re-expands on tap', async ({ page }) => { await page.goto('/post'); const details = page.locator('details.photos-collapse'); await expect(details).toHaveJSProperty('open', true); // open while empty await page.locator('input.filepond--browser').setInputFiles(TEST_HEIC); await waitForPhotoUpload(page, 1); // Settled → auto-collapsed, summary reflects the ready count. await expect(details).toHaveJSProperty('open', false); await expect(page.locator('.photos-collapse__summary')).toContainText('1 photo ready'); // Native
: clicking the summary re-expands for review. await page.locator('.photos-collapse__summary').click(); await expect(details).toHaveJSProperty('open', true); }); // ── Reorder: uploaded photos are renamed photo-1..N; no order field leaks ────── test('uploaded photos are renamed photo-1..N and the order field never hits frontmatter', async ({ page }) => { const tag = `rename-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await fillEditor(page, `photo rename test ${tag}`); await page.locator('input.filepond--browser').setInputFiles([TEST_JPG, TEST_JPG_B]); await waitForPhotoUpload(page, 2); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.notices')).toContainText('Entry posted successfully!', { timeout: 15_000 }); created.push(tag); const dir = findEntry(tag); expect(dir, 'Entry folder should exist').toBeTruthy(); const files = fs.readdirSync(dir); // Server renamed both uploads to the deterministic photo-N scheme (drag order). expect(files).toContain('photo-1.jpg'); expect(files).toContain('photo-2.jpg'); // The order is sent as a top-level POST key, so it must not appear in frontmatter. const mdName = files.find(f => /\.md$/.test(f)); const md = fs.readFileSync(path.join(dir, mdName), 'utf-8'); expect(md).not.toContain('photo_order'); }); // ── Date field is a native datetime-local picker, prefilled, and required ───── test('date field renders as a datetime-local picker, prefilled with now and required', async ({ page }) => { await page.goto('/post'); const date = page.locator('input[name="data[date]"]'); // Grav's deprecated datetime field used to fall back to a plain text box; // the theme override renders a real picker instead. await expect(date).toHaveAttribute('type', 'datetime-local'); // post-form.js prefills the current local time in the value format the // native input expects (YYYY-MM-DDTHH:MM). await expect(date).toHaveValue(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); // Clearing it and submitting must be blocked client-side (this is what keeps // an invalid/empty date from round-tripping to the server and wiping the // FilePond photo list on a re-render). await date.fill(''); await page.fill('input[name="data[title]"]', `UI date-${Date.now()}`); await fillEditor(page, 'datetime picker validation test'); await page.locator('.btn-post').evaluate(el => el.click()); await expect(date).toHaveClass(/field-invalid/); await expect(page.locator('.notices.success, .notices.green')).toHaveCount(0); }); // ── R18: Get Weather is gated on coordinates ────────────────────────────────── test('R18: Get Weather is disabled until Get Location provides coordinates', async ({ page, context }) => { await context.grantPermissions(['geolocation']); await context.setGeolocation({ latitude: 35.6812, longitude: 139.7671 }); await page.goto('/post'); await expect(page.locator('#get-weather')).toBeDisabled(); await page.click('#get-location'); await expect(page.locator('input[name="data[lat]"]')).toHaveValue(/35\.68/, { timeout: 5_000 }); await expect(page.locator('#get-weather')).toBeEnabled(); }); // ── R20: text draft survives a reload; photos need re-selecting ──────────────── test('R20: in-progress text is restored after a reload, with a photos hint', async ({ page }) => { await page.goto('/post'); await page.evaluate((k) => localStorage.removeItem(k), DRAFT_KEY); const marker = `draft-${Date.now()}`; await page.fill('input[name="data[title]"]', marker); await fillEditor(page, `Draft body ${marker}`); // Nudge an input event so the draft is written, then let it flush. await page.locator('input[name="data[title]"]').press('End'); await page.waitForTimeout(300); await page.reload(); await expect(page.locator('input[name="data[title]"]')).toHaveValue(marker); expect(await page.evaluate(() => window.postFormEditor.value())).toContain(marker); await expect(page.locator('.photo-reauth-hint')).toBeVisible(); await page.evaluate((k) => localStorage.removeItem(k), DRAFT_KEY); }); // ── Success confirmation: after a post, show a clear CTA the owner can act on ── test('post success shows a confirmation with a working "View your journal" link', async ({ page }) => { const tag = `success-${Date.now()}`; await page.goto('/post'); await page.fill('input[name="data[title]"]', `UI Test ${tag}`); await fillEditor(page, `success cta test ${tag}`); await page.locator('.btn-post').evaluate(el => el.click()); await expect(page.locator('.notices')).toContainText('Entry posted successfully!', { timeout: 15_000 }); created.push(tag); const panel = page.locator('.post-success'); await expect(panel).toBeVisible(); const view = panel.locator('.post-success__view'); await expect(view).toBeVisible(); // links into the active trip's journal (resolved from site.active_trip) await expect(view).toHaveAttribute('href', /\/trips\//); await expect(panel.locator('.post-success__again')).toBeVisible(); });