From f2c7594b236753e0b649b657f38431481eda8069 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 22 Sep 2025 22:32:56 +0200 Subject: [PATCH] Refetch gallery data after lightbox feedback (#29) --- .../gallery/PhotoGridWithLayouts.tsx | 1 + .../src/components/gallery/PhotoLightbox.tsx | 5 + tests/e2e/external-media-gallery.spec.ts | 173 ++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 tests/e2e/external-media-gallery.spec.ts diff --git a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx index be7757e..0415427 100644 --- a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx +++ b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx @@ -291,6 +291,7 @@ export const PhotoGridWithLayouts: React.FC = ({ protectionLevel={protectionLevel} useEnhancedProtection={useEnhancedProtection} initialShowFeedback={openFeedbackInitially} + onFeedbackChange={onFeedbackChange} /> )} diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index 183fa7e..8d07881 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -18,6 +18,7 @@ interface PhotoLightboxProps { protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum'; useEnhancedProtection?: boolean; initialShowFeedback?: boolean; + onFeedbackChange?: () => void; } export const PhotoLightbox: React.FC = ({ @@ -30,6 +31,7 @@ export const PhotoLightbox: React.FC = ({ protectionLevel = 'standard', useEnhancedProtection = false, initialShowFeedback = false, + onFeedbackChange, }) => { const [currentIndex, setCurrentIndex] = useState(initialIndex); const [zoom, setZoom] = useState(1); @@ -533,6 +535,9 @@ export const PhotoLightbox: React.FC = ({ gallerySlug={slug} showComments={true} className="space-y-4" + onFeedbackUpdate={() => { + if (onFeedbackChange) onFeedbackChange(); + }} /> diff --git a/tests/e2e/external-media-gallery.spec.ts b/tests/e2e/external-media-gallery.spec.ts new file mode 100644 index 0000000..c93cc76 --- /dev/null +++ b/tests/e2e/external-media-gallery.spec.ts @@ -0,0 +1,173 @@ +import { test, expect } from '@playwright/test'; + +const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@example.com'; +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234'; +const GALLERY_PASSWORD = process.env.GALLERY_PASSWORD || 'ExternalMediaPass!1'; + +async function createExternalGallery(page) { + const loginResponse = await page.request.post('/api/auth/admin/login', { + data: { + username: ADMIN_EMAIL, + password: ADMIN_PASSWORD, + }, + failOnStatusCode: false, + }); + expect(loginResponse.ok()).toBeTruthy(); + const { token } = await loginResponse.json(); + expect(token).toBeTruthy(); + + const eventName = `External Media Playwright ${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const eventDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .slice(0, 10); + + const createResponse = await page.request.post('/api/admin/events', { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: { + event_type: 'wedding', + event_name: eventName, + event_date: eventDate, + host_name: 'External Host', + host_email: 'host@example.com', + admin_email: ADMIN_EMAIL, + password: GALLERY_PASSWORD, + expiration_days: 30, + allow_user_uploads: false, + allow_downloads: true, + disable_right_click: false, + watermark_downloads: false, + feedback_enabled: true, + allow_ratings: true, + allow_likes: true, + allow_comments: true, + allow_favorites: true, + require_name_email: false, + moderate_comments: false, + show_feedback_to_guests: true, + source_mode: 'reference', + external_path: 'picsum-demo' + }, + failOnStatusCode: false, + }); + + if (!createResponse.ok()) { + const bodyText = await createResponse.text(); + throw new Error(`Failed to create event: ${createResponse.status()} ${bodyText}`); + } + const createdEvent = await createResponse.json(); + expect(createdEvent?.id).toBeTruthy(); + + const importResponse = await page.request.post(`/api/admin/external-media/events/${createdEvent.id}/import-external`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: { + external_path: 'picsum-demo', + recursive: true, + }, + failOnStatusCode: false, + }); + + expect(importResponse.ok()).toBeTruthy(); + const importBody = await importResponse.json(); + expect(importBody.imported).toBeGreaterThan(0); + + await page.request.put(`/api/admin/feedback/events/${createdEvent.id}/feedback-settings`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: { + feedback_enabled: true, + allow_ratings: true, + allow_likes: true, + allow_comments: true, + allow_favorites: true, + require_name_email: false, + moderate_comments: false, + show_feedback_to_guests: true, + }, + }); + + return { + shareLink: createdEvent.share_link, + slug: createdEvent.slug, + }; +} + +test.describe('External media gallery behavior', () => { + test.describe.configure({ mode: 'serial' }); + + test('Maintains session and favorites after reload', async ({ page, context }) => { + if (test.info().project.name.includes('mobile')) { + test.skip('Mobile viewport handling requires manual verification.'); + } + + const { shareLink, slug } = await createExternalGallery(page); + + await page.goto(shareLink); + await page.waitForLoadState('domcontentloaded'); + + const passwordField = page.getByPlaceholder(/gallery password/i).first(); + await expect(passwordField).toBeVisible(); + await passwordField.fill(GALLERY_PASSWORD); + await page.getByRole('button', { name: /View Gallery/i }).click(); + + const tiles = page.locator('.relative.group'); + await expect(tiles.first()).toBeVisible({ timeout: 20000 }); + + const initialTileCount = await tiles.count(); + expect(initialTileCount).toBeGreaterThan(0); + + const firstTile = tiles.first(); + await firstTile.scrollIntoViewIfNeeded(); + await firstTile.getByRole('button', { name: /View full size/i }).click(); + + await page.evaluate(() => { + const toggle = document.querySelector('[aria-label="Toggle feedback"]'); + if (toggle instanceof HTMLElement) toggle.click(); + }); + + const favoritesButtonInLightbox = page.getByRole('button', { name: /Add to favorites|Remove from favorites/ }).first(); + await expect(favoritesButtonInLightbox).toBeVisible(); + + const ariaLabel = await favoritesButtonInLightbox.getAttribute('aria-label'); + const isAlreadyFavorited = ariaLabel ? /Remove from favorites/i.test(ariaLabel) : false; + const refetchPromise = page.waitForResponse((res) => { + return res.request().method() === 'GET' && res.url().includes(`/api/gallery/${slug}/photos`); + }); + if (!isAlreadyFavorited) { + const favResponsePromise = page.waitForResponse((res) => { + return res.request().method() === 'POST' && res.url().includes(`/api/gallery/${slug}/photos/`); + }); + await favoritesButtonInLightbox.click(); + await Promise.all([favResponsePromise, refetchPromise]); + } else { + await refetchPromise; + } + + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + await page.getByRole('button', { name: 'Favorited' }).click(); + await expect(page.locator('.relative.group')).toHaveCount(1, { timeout: 15000 }); + + await page.reload(); + await page.waitForLoadState('networkidle'); + + await expect(page).toHaveURL(/\/gallery\//); + await expect(page.locator('.relative.group').first()).toBeVisible(); + + await page.getByRole('button', { name: 'Favorited' }).click(); + await expect(page.locator('.relative.group')).toHaveCount(1, { timeout: 15000 }); + + await page.getByRole('button', { name: 'All', exact: true }).click(); + await expect(page.locator('.relative.group')).toHaveCount(initialTileCount); + + const cookies = await context.cookies(); + expect(cookies.some((cookie) => cookie.name === 'gallery_token')).toBeTruthy(); + }); +});