Merge main into beta for release/beta-to-main
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
const GALLERY_PASSWORD = process.env.GALLERY_PASSWORD || 'PlaywrightGallery123!';
|
||||
|
||||
async function getAdminToken(page: Page): Promise<string> {
|
||||
const res = await page.request.post('/api/auth/admin/login', {
|
||||
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const { token } = await res.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
test.describe('Admin video upload (#203)', () => {
|
||||
test('Videos uploaded via admin have correct media_type and mime_type', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip();
|
||||
}
|
||||
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
// Create event
|
||||
const eventName = `PW Video ${Date.now()}`;
|
||||
const eventDate = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10);
|
||||
|
||||
const createRes = 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,
|
||||
customer_name: 'Playwright Host',
|
||||
customer_email: '[email protected]',
|
||||
admin_email: ADMIN_EMAIL,
|
||||
password: GALLERY_PASSWORD,
|
||||
expiration_days: 90,
|
||||
allow_downloads: true,
|
||||
},
|
||||
});
|
||||
expect(createRes.ok()).toBeTruthy();
|
||||
const event = await createRes.json();
|
||||
expect(event.id).toBeTruthy();
|
||||
|
||||
// Upload a video via admin endpoint
|
||||
const videoPath = path.join(process.cwd(), 'test-assets', 'test-video.mp4');
|
||||
expect(fs.existsSync(videoPath)).toBeTruthy();
|
||||
const buffer = fs.readFileSync(videoPath);
|
||||
|
||||
const uploadRes = await page.request.post(`/api/admin/events/${event.id}/upload`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
photos: { name: 'test-video.mp4', mimeType: 'video/mp4', buffer },
|
||||
category_id: 'individual',
|
||||
},
|
||||
});
|
||||
expect(uploadRes.ok()).toBeTruthy();
|
||||
const uploadBody = await uploadRes.json();
|
||||
expect(uploadBody.successCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Wait for background processing
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Fetch photos for this event via admin API
|
||||
const photosRes = await page.request.get(`/api/admin/photos/${event.id}/photos`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(photosRes.ok()).toBeTruthy();
|
||||
const photosBody = await photosRes.json();
|
||||
const photos = photosBody.photos || photosBody;
|
||||
expect(photos.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Find our video — the critical fix: media_type and mime_type must be set
|
||||
const video = photos.find((p: any) => p.media_type === 'video');
|
||||
expect(video).toBeTruthy();
|
||||
expect(video.media_type).toBe('video');
|
||||
expect(video.mime_type).toBe('video/mp4');
|
||||
// width/height/duration depend on ffprobe being available in the environment;
|
||||
// if present they should be positive, but we don't fail on missing ffprobe
|
||||
if (video.width !== null) {
|
||||
expect(video.width).toBeGreaterThan(0);
|
||||
expect(video.height).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Also upload an image and verify it gets media_type = 'image' with dimensions
|
||||
const imgBuffer = fs.readFileSync(path.join(process.cwd(), 'test-assets', 'img1.png'));
|
||||
const imgUploadRes = await page.request.post(`/api/admin/events/${event.id}/upload`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
photos: { name: 'img1.png', mimeType: 'image/png', buffer: imgBuffer },
|
||||
category_id: 'individual',
|
||||
},
|
||||
});
|
||||
expect(imgUploadRes.ok()).toBeTruthy();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Re-fetch and verify image has correct media_type and dimensions
|
||||
const photosRes2 = await page.request.get(`/api/admin/photos/${event.id}/photos`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(photosRes2.ok()).toBeTruthy();
|
||||
const photosBody2 = await photosRes2.json();
|
||||
const photos2 = photosBody2.photos || photosBody2;
|
||||
|
||||
const image = photos2.find((p: any) => p.media_type === 'image');
|
||||
expect(image).toBeTruthy();
|
||||
expect(image.media_type).toBe('image');
|
||||
expect(image.width).toBeGreaterThan(0);
|
||||
expect(image.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
const GALLERY_PASSWORD = process.env.GALLERY_PASSWORD || 'PlaywrightGallery123!';
|
||||
|
||||
async function getAdminToken(page: Page): Promise<string> {
|
||||
const res = await page.request.post('/api/auth/admin/login', {
|
||||
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const { token } = await res.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
async function createGalleryPremiumEvent(page: Page) {
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
const eventName = `PW SelectAll ${Date.now()}`;
|
||||
const eventDate = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10);
|
||||
|
||||
const createRes = 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,
|
||||
customer_name: 'Playwright Host',
|
||||
customer_email: '[email protected]',
|
||||
admin_email: ADMIN_EMAIL,
|
||||
password: GALLERY_PASSWORD,
|
||||
expiration_days: 90,
|
||||
allow_downloads: true,
|
||||
gallery_theme: 'gallery-premium',
|
||||
},
|
||||
});
|
||||
expect(createRes.ok()).toBeTruthy();
|
||||
const event = await createRes.json();
|
||||
|
||||
// Upload 3 images so we can verify select-all picks all of them
|
||||
const imagePaths = ['img1.png', 'img2.png', 'img1.png'].map((f) =>
|
||||
path.join(process.cwd(), 'test-assets', f)
|
||||
);
|
||||
|
||||
for (const imagePath of imagePaths) {
|
||||
const buffer = fs.readFileSync(imagePath);
|
||||
const uploadRes = await page.request.post(`/api/admin/events/${event.id}/upload`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
photos: { name: path.basename(imagePath), mimeType: 'image/png', buffer },
|
||||
category_id: 'individual',
|
||||
},
|
||||
});
|
||||
expect(uploadRes.ok()).toBeTruthy();
|
||||
}
|
||||
|
||||
return { shareLink: event.share_link, slug: event.slug, token };
|
||||
}
|
||||
|
||||
test.describe('Gallery-Premium Select All (#220)', () => {
|
||||
test('Select All button selects all photos in one click', async ({ page, browserName }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip();
|
||||
}
|
||||
|
||||
const { shareLink } = await createGalleryPremiumEvent(page);
|
||||
|
||||
// Navigate to gallery
|
||||
await page.goto(shareLink);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Handle password if needed
|
||||
const passwordField = page.getByPlaceholder(/gallery password/i).first();
|
||||
if (await passwordField.count()) {
|
||||
await passwordField.fill(GALLERY_PASSWORD);
|
||||
try {
|
||||
await page.getByRole('button', { name: /View Gallery/i }).click({ timeout: 5000 });
|
||||
} catch {
|
||||
// Token may auto-auth
|
||||
}
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
// Wait for photos to render
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Look for download button to enter selection mode
|
||||
const downloadBtn = page.getByRole('button', { name: /Download/i }).first();
|
||||
await expect(downloadBtn).toBeVisible({ timeout: 15000 });
|
||||
await downloadBtn.click();
|
||||
|
||||
// Now look for "Select All" button
|
||||
const selectAllBtn = page.getByRole('button', { name: /Select All/i }).first();
|
||||
await expect(selectAllBtn).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click Select All once
|
||||
await selectAllBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify all photos are selected - check for checkmarks or selected state
|
||||
// The selection count or "Deselect All" text should appear
|
||||
const deselectAllBtn = page.getByRole('button', { name: /Deselect All/i }).first();
|
||||
await expect(deselectAllBtn).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify: clicking Deselect All should clear selection
|
||||
await deselectAllBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Select All should be visible again
|
||||
await expect(selectAllBtn).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
|
||||
async function getAdminToken(page: Page): Promise<string> {
|
||||
const res = await page.request.post('/api/auth/admin/login', {
|
||||
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const { token } = await res.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
test.describe('Photo Dimensions Repair (#180)', () => {
|
||||
test('Status endpoint returns dimension counts', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip();
|
||||
}
|
||||
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
const statusRes = await page.request.get('/api/admin/photos/repair-dimensions/status', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(statusRes.ok()).toBeTruthy();
|
||||
|
||||
const status = await statusRes.json();
|
||||
expect(status).toHaveProperty('total');
|
||||
expect(status).toHaveProperty('withDimensions');
|
||||
expect(status).toHaveProperty('withoutDimensions');
|
||||
expect(status).toHaveProperty('isRunning');
|
||||
expect(typeof status.total).toBe('number');
|
||||
expect(typeof status.isRunning).toBe('boolean');
|
||||
});
|
||||
|
||||
test('Repair endpoint runs and returns immediately', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip();
|
||||
}
|
||||
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
const repairRes = await page.request.post('/api/admin/photos/repair-dimensions', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(repairRes.ok()).toBeTruthy();
|
||||
|
||||
const body = await repairRes.json();
|
||||
expect(body).toHaveProperty('message');
|
||||
expect(body).toHaveProperty('count');
|
||||
|
||||
// Wait for background job to complete
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check status after repair
|
||||
const statusRes = await page.request.get('/api/admin/photos/repair-dimensions/status', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(statusRes.ok()).toBeTruthy();
|
||||
const status = await statusRes.json();
|
||||
expect(status.isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
|
||||
async function getAdminToken(page: import('@playwright/test').Page): Promise<string> {
|
||||
const loginRes = await page.request.post('/api/auth/admin/login', {
|
||||
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
const body = await loginRes.json();
|
||||
return body.token;
|
||||
}
|
||||
|
||||
async function adminLogin(page: import('@playwright/test').Page) {
|
||||
await page.goto('/admin/login');
|
||||
await page.getByLabel(/Email|E-Mail/i).fill(ADMIN_EMAIL);
|
||||
await page.getByLabel(/Password|Passwort/i).fill(ADMIN_PASSWORD);
|
||||
await page.getByRole('button', { name: /Sign In|Log in|Anmelden/i }).click();
|
||||
await expect(page.getByRole('heading', { name: /Dashboard|Übersicht/i })).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
test.describe('Upload batch size setting', () => {
|
||||
test('setting exists in DB via API with default value 95', async ({ page }) => {
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
const res = await page.request.get('/api/admin/settings', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const settings = await res.json();
|
||||
expect(settings.general_max_upload_batch_size_mb).toBe(95);
|
||||
});
|
||||
|
||||
test('setting appears in General settings UI and can be changed', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Settings UI validated on desktop viewport');
|
||||
}
|
||||
|
||||
await adminLogin(page);
|
||||
await page.goto('/admin/settings');
|
||||
|
||||
// Find the batch size input by its nearby label text
|
||||
const batchSizeLabel = page.locator('label', { hasText: /Max Upload Batch Size|Max\. Upload-Paketgröße/i });
|
||||
await expect(batchSizeLabel).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// The input is a sibling within the same container
|
||||
const batchSizeInput = batchSizeLabel.locator('..').locator('input[type="number"]');
|
||||
await expect(batchSizeInput).toBeVisible();
|
||||
await expect(batchSizeInput).toHaveValue('95');
|
||||
|
||||
// Change value to 50
|
||||
await batchSizeInput.fill('50');
|
||||
|
||||
// Save general settings
|
||||
const saveButton = page.getByRole('button', { name: /Save General Settings|Allgemeine Einstellungen speichern/i });
|
||||
await saveButton.click();
|
||||
|
||||
// Wait for success toast
|
||||
await expect(page.locator('.Toastify__toast').filter({ hasText: /(Settings saved|Einstellungen gespeichert)/i })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Reload and verify persisted
|
||||
await page.reload();
|
||||
const batchSizeLabelAfter = page.locator('label', { hasText: /Max Upload Batch Size|Max\. Upload-Paketgröße/i });
|
||||
await expect(batchSizeLabelAfter).toBeVisible({ timeout: 10000 });
|
||||
const batchSizeInputAfter = batchSizeLabelAfter.locator('..').locator('input[type="number"]');
|
||||
await expect(batchSizeInputAfter).toHaveValue('50');
|
||||
|
||||
// Revert to default
|
||||
await batchSizeInputAfter.fill('95');
|
||||
await page.getByRole('button', { name: /Save General Settings|Allgemeine Einstellungen speichern/i }).click();
|
||||
await expect(page.locator('.Toastify__toast').filter({ hasText: /(Settings saved|Einstellungen gespeichert)/i })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('setting is used for upload chunking via API', async ({ page }) => {
|
||||
const token = await getAdminToken(page);
|
||||
|
||||
// Set batch size to a small value
|
||||
const updateRes = await page.request.put('/api/admin/settings/general', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { general_max_upload_batch_size_mb: 10 },
|
||||
});
|
||||
expect(updateRes.ok()).toBeTruthy();
|
||||
|
||||
// Verify the setting was saved
|
||||
const getRes = await page.request.get('/api/admin/settings', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(getRes.ok()).toBeTruthy();
|
||||
const settings = await getRes.json();
|
||||
expect(settings.general_max_upload_batch_size_mb).toBe(10);
|
||||
|
||||
// Revert to default
|
||||
await page.request.put('/api/admin/settings/general', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { general_max_upload_batch_size_mb: 95 },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user