215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
import { test, expect, 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!';
|
|
|
|
async function createEventWithPhotos(page: Page, adminToken?: string, attempt = 1) {
|
|
const api = page.request;
|
|
let token = adminToken;
|
|
|
|
if (!token) {
|
|
const loginResponse = await api.post('/api/auth/admin/login', {
|
|
data: {
|
|
username: ADMIN_EMAIL,
|
|
password: ADMIN_PASSWORD,
|
|
},
|
|
});
|
|
expect(loginResponse.ok()).toBeTruthy();
|
|
const loginData = await loginResponse.json();
|
|
token = loginData.token;
|
|
expect(token).toBeTruthy();
|
|
}
|
|
|
|
const eventName = `Playwright Smoke ${Date.now()}`;
|
|
const eventDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
.toISOString()
|
|
.slice(0, 10);
|
|
|
|
if (!token) {
|
|
throw new Error('Failed to acquire admin token');
|
|
}
|
|
|
|
const eventResponse = await api.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: 'host@example.com',
|
|
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,
|
|
},
|
|
});
|
|
if (!eventResponse.ok()) {
|
|
const message = await eventResponse.text();
|
|
if (
|
|
attempt < 3 &&
|
|
/UNIQUE constraint failed: events\.slug/i.test(message || '')
|
|
) {
|
|
await page.waitForTimeout(150);
|
|
return createEventWithPhotos(page, token, attempt + 1);
|
|
}
|
|
throw new Error(`Event creation failed: ${eventResponse.status()} ${message}`);
|
|
}
|
|
const event = await eventResponse.json();
|
|
|
|
const imagePath = path.join(process.cwd(), 'test-assets', 'img1.png');
|
|
const buffer = fs.readFileSync(imagePath);
|
|
const uploadResponse = await api.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(uploadResponse.ok()).toBeTruthy();
|
|
|
|
return {
|
|
event,
|
|
shareLink: event.share_link,
|
|
slug: event.slug,
|
|
adminToken: token,
|
|
};
|
|
}
|
|
|
|
async function updateShortGallerySetting(page: Page, adminToken: string, enabled: boolean) {
|
|
const response = await page.request.put('/api/admin/settings/general', {
|
|
headers: {
|
|
Authorization: `Bearer ${adminToken}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {
|
|
general_short_gallery_urls: enabled,
|
|
},
|
|
});
|
|
expect(response.ok()).toBeTruthy();
|
|
}
|
|
|
|
async function openGalleryShareLink(page: Page, shareLink: string) {
|
|
await page.context().clearCookies();
|
|
await page.goto(shareLink);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
try {
|
|
await page.getByText(/Enter Gallery Password/i).first().waitFor({ timeout: 5000 });
|
|
} catch {
|
|
// No password prompt shown (public gallery)
|
|
}
|
|
|
|
let passwordEntered = false;
|
|
const passwordTextbox = page.getByRole('textbox', { name: /password/i }).first();
|
|
if (await passwordTextbox.count()) {
|
|
await passwordTextbox.fill(GALLERY_PASSWORD);
|
|
passwordEntered = true;
|
|
}
|
|
|
|
const galleryPasswordField = page.getByPlaceholder(/gallery password/i);
|
|
if (!passwordEntered && await galleryPasswordField.count()) {
|
|
await galleryPasswordField.fill(GALLERY_PASSWORD);
|
|
passwordEntered = true;
|
|
} else if (!passwordEntered) {
|
|
const genericPasswordField = page.getByPlaceholder(/password/i).first();
|
|
if (await genericPasswordField.count()) {
|
|
await genericPasswordField.fill(GALLERY_PASSWORD);
|
|
passwordEntered = true;
|
|
} else {
|
|
const labelledPasswordField = page.getByLabel(/password/i).first();
|
|
if (await labelledPasswordField.count()) {
|
|
await labelledPasswordField.fill(GALLERY_PASSWORD);
|
|
passwordEntered = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!passwordEntered) {
|
|
const fallbackPasswordField = page.locator('input').first();
|
|
if (await fallbackPasswordField.count()) {
|
|
await fallbackPasswordField.fill(GALLERY_PASSWORD);
|
|
passwordEntered = true;
|
|
}
|
|
}
|
|
|
|
const viewButton = page.getByRole('button', { name: /View Gallery/i });
|
|
if (await viewButton.count()) {
|
|
try {
|
|
await viewButton.click({ noWaitAfter: true, timeout: 2000 });
|
|
} catch {
|
|
// Already navigated into gallery view.
|
|
}
|
|
}
|
|
|
|
const tiles = page.locator('.relative.group');
|
|
await expect(tiles.first()).toBeVisible({ timeout: 20000 });
|
|
return tiles;
|
|
}
|
|
|
|
test('admin login and gallery viewing smoke test', async ({ page }) => {
|
|
const { shareLink, adminToken } = await createEventWithPhotos(page);
|
|
|
|
// Admin UI login
|
|
await page.goto('/admin/login');
|
|
const emailField = page.getByLabel(/Email/i);
|
|
if (await emailField.count()) {
|
|
await emailField.fill(ADMIN_EMAIL);
|
|
await page.getByLabel(/Password/i).fill(ADMIN_PASSWORD);
|
|
await page.getByRole('button', { name: /Sign In|Log in/i }).click();
|
|
}
|
|
await expect(page.getByRole('heading', { name: /Dashboard/i })).toBeVisible({ timeout: 20000 });
|
|
|
|
let resetToken = adminToken;
|
|
try {
|
|
// Verify long-form share link works
|
|
const tiles = await openGalleryShareLink(page, shareLink);
|
|
await tiles.first().hover();
|
|
await tiles.first().getByRole('button', { name: /View full size/i }).click();
|
|
await expect(page.getByRole('button', { name: /Close/i })).toBeVisible();
|
|
await page.getByRole('button', { name: /Close/i }).click();
|
|
|
|
// Enable short gallery URLs
|
|
await updateShortGallerySetting(page, adminToken, true);
|
|
|
|
const settingsResponse = await page.request.get('/api/admin/settings', {
|
|
headers: {
|
|
Authorization: `Bearer ${adminToken}`,
|
|
},
|
|
});
|
|
expect(settingsResponse.ok()).toBeTruthy();
|
|
const adminSettings = await settingsResponse.json();
|
|
expect(adminSettings.general_short_gallery_urls === true || adminSettings.general_short_gallery_urls === 'true').toBeTruthy();
|
|
|
|
const { shareLink: shortShareLink, event: shortEvent } = await createEventWithPhotos(page, adminToken);
|
|
expect(shortShareLink).toMatch(/\/gallery\/[0-9a-fA-F]{32}$/);
|
|
expect(shortShareLink).not.toContain(shortEvent.slug);
|
|
|
|
// Verify short share link works
|
|
await openGalleryShareLink(page, shortShareLink);
|
|
|
|
// Legacy share link should still work after enabling short URLs
|
|
await openGalleryShareLink(page, shareLink);
|
|
} finally {
|
|
await updateShortGallerySetting(page, resetToken, false).catch(() => {
|
|
/* noop */
|
|
});
|
|
}
|
|
});
|