From db2f5da66a2b72c3c47e5326b8f16209205d305e Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 17 Sep 2025 23:15:28 +0200 Subject: [PATCH] Ensure gallery comment filter hides moderated comments --- .../src/components/gallery/GalleryView.tsx | 17 +- playwright.config.ts | 18 ++ test-assets/img1.png | Bin 0 -> 212 bytes test-assets/img2.png | Bin 0 -> 212 bytes tests/e2e/gallery-feedback-filter.spec.ts | 226 ++++++++++++++++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 playwright.config.ts create mode 100644 test-assets/img1.png create mode 100644 test-assets/img2.png create mode 100644 tests/e2e/gallery-feedback-filter.spec.ts diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 7101194..7cc597f 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -266,6 +266,21 @@ export const GalleryView: React.FC = ({ slug, event }) => { ); } + // Apply feedback filter + switch (filterType) { + case 'liked': + photos = photos.filter(photo => (photo.like_count || 0) > 0); + break; + case 'rated': + photos = photos.filter(photo => (photo.average_rating || 0) > 0 || (photo.total_ratings || 0) > 0); + break; + case 'commented': + photos = photos.filter(photo => (photo.comment_count || 0) > 0); + break; + default: + break; + } + // Apply sorting photos.sort((a, b) => { switch (sortBy) { @@ -298,7 +313,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { } return photos; - }, [data?.photos, selectedCategoryId, searchTerm, sortBy, watermarkEnabled, slug]); + }, [data?.photos, selectedCategoryId, searchTerm, sortBy, watermarkEnabled, slug, filterType]); // Check if downloads are allowed (both event setting and not expired) const allowDownloads = !isExpired && (data?.event?.allow_downloads === true); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6048922 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'tests/e2e', + timeout: 60_000, + retries: 0, + use: { + baseURL: 'http://localhost:3000', + headless: true, + viewport: { width: 1280, height: 800 }, + ignoreHTTPSErrors: true, + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, + ], +}); + diff --git a/test-assets/img1.png b/test-assets/img1.png new file mode 100644 index 0000000000000000000000000000000000000000..23f25487e761f79968e846c39b38e99a66fb738a GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGH$&H|6fVg?507a+{IwK^ypDA?xd z;uumf=k0k$P6h^^!yA5>?>#Ni?98&|kjdAkKMk3$Sbs8JzQX;9dD%+wC+t4K`X~5j sEQ^~UZ{WLE!9HPDm1ErjDkv;ldFR8UvrhZ0fi7b3boFyt=akR{0Q68$w*UYD literal 0 HcmV?d00001 diff --git a/test-assets/img2.png b/test-assets/img2.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf1a604bce7a63ffbc9d43517e5477d332981dd GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLGH$&H|6fVg?507a+{IwK^ypDA?xd z;uumf=k0k$P6h^^!yA5>?>#Ni?98&|gvHmUKMk3$Sbs8JzQX;9dD%+wC+t4K`X~5j uEQ^~UZ{WLE!9HPDm1Erj6N(i|lpX)`;Ze`Fj!K}57(8A5T-G@yGywqi22i>H literal 0 HcmV?d00001 diff --git a/tests/e2e/gallery-feedback-filter.spec.ts b/tests/e2e/gallery-feedback-filter.spec.ts new file mode 100644 index 0000000..7f865af --- /dev/null +++ b/tests/e2e/gallery-feedback-filter.spec.ts @@ -0,0 +1,226 @@ +import { test, expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; + +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 || 'PlaywrightGallery123!'; + +interface GallerySetupResult { + shareLink: string; + slug: string; + allPhotosData: { + event: any; + categories?: any; + photos: Array<{ id: number; filename: string; comment_count?: number }>; + }; +} + +async function createGalleryWithModeratedComments(page: Page): Promise { + 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 = `Playwright Feedback Filter ${Date.now()}`; + 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: 'Playwright 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: true, + show_feedback_to_guests: true, + }, + failOnStatusCode: false, + }); + + expect(createResponse.ok()).toBeTruthy(); + const createdEvent = await createResponse.json(); + expect(createdEvent?.id).toBeTruthy(); + + const imagePaths = ['img1.png', 'img2.png']; + const photoIds: number[] = []; + + for (const file of imagePaths) { + const imagePath = path.join(process.cwd(), 'test-assets', file); + const buffer = fs.readFileSync(imagePath); + const uploadResponse = await page.request.post( + `/api/admin/events/${createdEvent.id}/upload`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + multipart: { + photos: { + name: path.basename(imagePath), + mimeType: 'image/png', + buffer, + }, + category_id: 'individual', + }, + failOnStatusCode: false, + } + ); + expect(uploadResponse.ok()).toBeTruthy(); + const uploadJson = await uploadResponse.json(); + const uploaded = uploadJson?.photos?.[0]; + expect(uploaded?.id).toBeTruthy(); + photoIds.push(uploaded.id); + } + + expect(photoIds.length).toBeGreaterThanOrEqual(2); + + const galleryAuthResponse = await page.request.post('/api/auth/gallery/verify', { + data: { + slug: createdEvent.slug, + password: GALLERY_PASSWORD, + }, + failOnStatusCode: false, + }); + expect(galleryAuthResponse.ok()).toBeTruthy(); + const { token: galleryToken } = await galleryAuthResponse.json(); + expect(galleryToken).toBeTruthy(); + + // Submit an approved comment (after moderation) + const approvedCommentResponse = await page.request.post( + `/api/gallery/${createdEvent.slug}/photos/${photoIds[0]}/feedback`, + { + headers: { + Authorization: `Bearer ${galleryToken}`, + 'Content-Type': 'application/json', + }, + data: { + feedback_type: 'comment', + comment_text: 'Approved comment', + guest_name: 'Approved Guest', + guest_email: 'approved@example.com', + }, + failOnStatusCode: false, + } + ); + expect(approvedCommentResponse.ok()).toBeTruthy(); + const approvedComment = await approvedCommentResponse.json(); + expect(approvedComment?.id).toBeTruthy(); + + const approveModeration = await page.request.put( + `/api/admin/feedback/feedback/${approvedComment.id}/approve`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + failOnStatusCode: false, + } + ); + expect(approveModeration.ok()).toBeTruthy(); + + // Submit a second comment that remains pending + const pendingCommentResponse = await page.request.post( + `/api/gallery/${createdEvent.slug}/photos/${photoIds[1]}/feedback`, + { + headers: { + Authorization: `Bearer ${galleryToken}`, + 'Content-Type': 'application/json', + }, + data: { + feedback_type: 'comment', + comment_text: 'Pending comment', + guest_name: 'Pending Guest', + guest_email: 'pending@example.com', + }, + failOnStatusCode: false, + } + ); + expect(pendingCommentResponse.ok()).toBeTruthy(); + + const allPhotosResponse = await page.request.get(`/api/gallery/${createdEvent.slug}/photos`, { + headers: { + Authorization: `Bearer ${galleryToken}`, + }, + failOnStatusCode: false, + }); + expect(allPhotosResponse.ok()).toBeTruthy(); + const allPhotosData = await allPhotosResponse.json(); + expect(Array.isArray(allPhotosData?.photos)).toBeTruthy(); + + return { + shareLink: createdEvent.share_link, + slug: createdEvent.slug, + allPhotosData, + }; +} + +test.describe('Gallery feedback filter', () => { + test('Comment filter hides photos without approved comments', async ({ page }) => { + const { shareLink, slug, allPhotosData } = await createGalleryWithModeratedComments(page); + + const approvedPhotos = allPhotosData.photos.filter((photo) => (photo.comment_count || 0) > 0); + expect(approvedPhotos.length).toBeGreaterThan(0); + + await page.route(`**/api/gallery/${slug}/photos**`, async (route) => { + const url = new URL(route.request().url()); + if (url.searchParams.get('filter') === 'commented') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(allPhotosData), + }); + await page.unroute(`**/api/gallery/${slug}/photos**`); + } else { + await route.continue(); + } + }); + + await page.goto(shareLink); + await page.waitForLoadState('domcontentloaded'); + + const passwordField = page.getByPlaceholder(/gallery password/i).first(); + if (await passwordField.count()) { + await passwordField.fill(GALLERY_PASSWORD); + await page.getByRole('button', { name: /View Gallery/i }).click(); + } + + await page.waitForLoadState('networkidle'); + + const tiles = page.locator('.relative.group'); + await expect(tiles.first()).toBeVisible({ timeout: 20000 }); + await expect(tiles).toHaveCount(allPhotosData.photos.length); + + await page.getByRole('button', { name: /Commented/i }).click(); + + await expect(tiles).toHaveCount(approvedPhotos.length, { timeout: 20000 }); + + for (const pending of allPhotosData.photos.filter((photo) => (photo.comment_count || 0) === 0)) { + await expect(page.getByAltText(pending.filename)).not.toBeVisible({ timeout: 1000 }); + } + }); +});