// @ts-check // Tests: AN1–AN2 — 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); });