test(post-form): add edit/delete/anon coverage and fix stale photo-gate assumptions

New specs: edit-mode (ES1 save round-trip + ES2/ES3 prefill 404/500 states),
delete-flow (DEL1-3 happy/cancel/failed), anon-view (AN1 no owner controls,
AN2 draft hidden from anon), photo-editor (live add/delete/reorder). Existing:
P3-P8 now attach a photo to satisfy the create photo-gate; V3 picker cap 4->6.
Full post suite 38/38, stable across parallel (3-worker) runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
This commit is contained in:
2026-07-05 23:42:45 +02:00
co-authored by Claude Opus 4.8
parent 7534d7d178
commit d3c17791b7
7 changed files with 565 additions and 27 deletions
+80
View File
@@ -0,0 +1,80 @@
// @ts-check
// Tests: AN1AN2 — the anonymous (logged-out) visitor's view of the active trip.
//
// Every other spec runs as the authenticated owner, so nothing guards the
// owner/anon boundary. These assert the two things that boundary must enforce
// (R5, KTD7):
// - AN1: owner-only controls (Edit/Delete, data-entry-route) never render for
// an anonymous visitor, even though published entries are visible.
// - AN2: an unpublished DRAFT is shown to the owner (with a badge) but is
// completely absent for an anonymous visitor.
//
// The whole file runs UNauthenticated by clearing storageState. AN2 spins up a
// short-lived authenticated context to create the draft fixture and confirm the
// owner-visible side.
const { test, expect } = require('@playwright/test');
const path = require('path');
const { fillEditor, waitForPhotoUpload, cleanupEntry, findEntry, ACTIVE_TRIP_URL } = require('../helpers');
const TEST_PHOTO = path.join(__dirname, '../../fixtures/test-photo.jpg');
const AUTH_STATE = 'tests/.auth/user.json';
const BASE = process.env.GRAV_BASE_URL || 'http://localhost:8081';
// Run this file with NO owner session.
test.use({ storageState: { cookies: [], origins: [] } });
const created = [];
test.afterAll(() => created.forEach(cleanupEntry));
// ── AN1: anonymous visitor sees content but no owner controls ─────────────────
test('AN1: an anonymous visitor sees published entries but no owner controls', async ({ page }) => {
await page.goto(ACTIVE_TRIP_URL);
// The demo trip has published journal cards — content is public.
await expect(page.locator('.journal-post').first()).toBeVisible();
// …but none of the owner-only affordances are present in the markup.
await expect(page.locator('.journal-post-actions')).toHaveCount(0);
await expect(page.locator('.entry-action--edit')).toHaveCount(0);
await expect(page.locator('.entry-action--delete')).toHaveCount(0);
await expect(page.locator('[data-entry-route]')).toHaveCount(0);
});
// ── AN2: a draft is owner-only ────────────────────────────────────────────────
test('AN2: a draft entry is shown to the owner but hidden from an anonymous visitor', async ({ page, browser }) => {
const tag = `draft-${Date.now()}`;
// Create an UNPUBLISHED entry as the owner, in a separate authed context.
const owner = await browser.newContext({ storageState: AUTH_STATE, baseURL: BASE });
const op = await owner.newPage();
await op.goto('/post');
await op.fill('input[name="data[title]"]', `UI Test ${tag}`);
await fillEditor(op, `Draft body ${tag}. Safe to delete.`);
await op.locator('input.filepond--browser').setInputFiles(TEST_PHOTO);
await waitForPhotoUpload(op);
// Flip Published → No. The toggle renders as a visually-hidden radio pair
// behind "More options", so set the state directly and fire `change` (what a
// real click would do) rather than fighting the visibility gate.
await op.evaluate(() => {
const off = document.querySelector('input[name="data[published]"][value="0"]');
off.checked = true;
off.dispatchEvent(new Event('change', { bubbles: true }));
});
await op.locator('.btn-post').evaluate(el => el.click());
await expect(op.locator('.form-messages, .notices')).toContainText(
'Entry posted successfully!', { timeout: 15_000 });
created.push(tag);
expect(findEntry(tag), 'draft fixture should exist on disk').not.toBeNull();
// Owner side: the draft appears in the feed WITH a Draft badge.
await op.goto(ACTIVE_TRIP_URL);
const ownerCard = op.locator('.journal-post', { hasText: tag });
await expect(ownerCard).toHaveCount(1);
await expect(ownerCard.locator('.journal-draft-badge')).toBeVisible();
await owner.close();
// Anonymous side (the default page fixture): the draft is nowhere to be seen.
await page.goto(ACTIVE_TRIP_URL);
await expect(page.locator('.journal-post', { hasText: tag })).toHaveCount(0);
await expect(page.locator('body')).not.toContainText(tag);
});