Ensure gallery comment filter hides moderated comments
Mirror to GitHub / mirror (push) Successful in 39s
Test and Lint / backend-test (push) Successful in 1m38s
Test and Lint / frontend-test (push) Successful in 2m25s
Version and Release / version-bump (push) Successful in 1m46s
Version and Release / trigger-drone (push) Successful in 4s

This commit is contained in:
2025-09-17 23:15:28 +02:00
parent 19f8facc49
commit db2f5da66a
5 changed files with 260 additions and 1 deletions
@@ -266,6 +266,21 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ 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 // Apply sorting
photos.sort((a, b) => { photos.sort((a, b) => {
switch (sortBy) { switch (sortBy) {
@@ -298,7 +313,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
} }
return photos; 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) // Check if downloads are allowed (both event setting and not expired)
const allowDownloads = !isExpired && (data?.event?.allow_downloads === true); const allowDownloads = !isExpired && (data?.event?.allow_downloads === true);
+18
View File
@@ -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'] } },
],
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

+226
View File
@@ -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<GallerySetupResult> {
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 });
}
});
});