Harden auth cookies and fix native schema for event creation
Mirror to GitHub / mirror (push) Successful in 45s
Test and Lint / backend-test (push) Successful in 1m37s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s

This commit is contained in:
2025-09-18 15:58:58 +02:00
parent bda76ff513
commit 71e7179145
35 changed files with 1055 additions and 381 deletions
+43
View File
@@ -0,0 +1,43 @@
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';
function randomSuffix() {
return Math.random().toString(36).slice(2, 8);
}
test('admin can create event via UI', async ({ page }) => {
const eventName = `UI Playwright ${randomSuffix()}`;
const hostEmail = `host+${randomSuffix()}@example.com`;
// Login
await page.goto('/admin/login');
await page.getByLabel(/Email/i).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 });
// Navigate to create event page
const createButton = page.getByRole('button', { name: /Create Event/i });
if (await createButton.count()) {
await createButton.first().click();
} else {
await page.goto('/admin/events/new');
}
await expect(page.getByRole('heading', { name: /^Create$/i })).toBeVisible({ timeout: 10000 });
await page.getByLabel(/Event Name/i).fill(eventName);
await page.getByLabel(/Host Name/i).fill('Host User');
await page.getByLabel(/Event Date/i).fill('2025-12-31');
await page.getByLabel(/Host Email/i).fill(hostEmail);
await page.getByLabel(/Admin Email/i).fill(ADMIN_EMAIL);
await page.getByLabel(/Gallery Password/i).fill('UiPlay123!');
await page.getByLabel(/Confirm Password/i).fill('UiPlay123!');
await page.getByRole('button', { name: /Create Event/i }).click();
await expect(page).toHaveURL(/\/admin\/events\//, { timeout: 20000 });
await expect(page.getByRole('heading', { name: eventName })).toBeVisible();
});
+100
View File
@@ -0,0 +1,100 @@
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) {
const api = page.request;
const loginResponse = await api.post('/api/auth/admin/login', {
data: {
username: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
},
});
expect(loginResponse.ok()).toBeTruthy();
const { token } = await loginResponse.json();
expect(token).toBeTruthy();
const eventName = `Playwright Smoke ${Date.now()}`;
const eventDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);
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,
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,
},
});
expect(eventResponse.ok()).toBeTruthy();
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,
};
}
test('admin login and gallery viewing smoke test', async ({ page }) => {
const { shareLink } = 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 });
// Visit gallery share link and authenticate
await page.goto(shareLink);
const passwordField = page.getByPlaceholder(/gallery password/i);
await passwordField.fill(GALLERY_PASSWORD);
await page.getByRole('button', { name: /View Gallery/i }).click();
// Wait for photos grid to appear
const tiles = page.locator('.relative.group');
await expect(tiles.first()).toBeVisible({ timeout: 20000 });
// Open lightbox to ensure media renders
await tiles.first().hover();
await tiles.first().getByRole('button', { name: /View full size/i }).click();
await expect(page.getByRole('button', { name: /Close/i })).toBeVisible();
});
+180
View File
@@ -0,0 +1,180 @@
import { test, expect } 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 ensureGalleryWithPhotos(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 = `Playwright MCP ${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: 90,
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,
},
failOnStatusCode: false,
});
expect(createResponse.ok()).toBeTruthy();
const createdEvent = await createResponse.json();
expect(createdEvent?.id).toBeTruthy();
const imagePaths = ['img1.png', 'img2.png'].map((file) =>
path.join(process.cwd(), 'test-assets', file)
);
for (const imagePath of imagePaths) {
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();
}
return {
shareLink: createdEvent.share_link,
slug: createdEvent.slug,
};
}
test.describe('Gallery grid tile quick actions', () => {
test('Each tile: open, download, comment, like with immediate UI', async ({ page }) => {
const { shareLink } = await ensureGalleryWithPhotos(page);
await page.goto(shareLink);
const gallery = page;
await gallery.waitForLoadState('domcontentloaded');
await gallery.waitForURL(/\/gallery\//);
const passwordField = gallery.getByPlaceholder(/gallery password/i).first();
if (await passwordField.count()) {
await passwordField.fill(GALLERY_PASSWORD);
await gallery.getByRole('button', { name: /View Gallery/i }).click();
await gallery.waitForLoadState('networkidle');
}
// Ensure grid tiles rendered
const tiles = gallery.locator('.relative.group');
await expect(tiles.first()).toBeVisible({ timeout: 20000 });
const tileCount = await tiles.count();
expect(tileCount).toBeGreaterThan(0);
// Limit to a few tiles to keep test time sensible
const N = Math.min(tileCount, 3);
for (let i = 0; i < N; i++) {
const tile = tiles.nth(i);
await tile.scrollIntoViewIfNeeded();
// On desktop, actions show on hover
await tile.hover({ force: true });
// Actions should be present
const openBtn = tile.getByRole('button', { name: /View full size/i });
await expect(openBtn).toBeVisible();
const likeBtn = tile.getByRole('button', { name: /Like photo/i }).first();
await expect(likeBtn).toBeVisible();
const commentBtn = tile.getByRole('button', { name: /Comment on photo|Comment/i }).first();
await expect(commentBtn).toBeVisible();
const downloadBtn = tile.getByRole('button', { name: /Download photo/i }).first();
await expect(downloadBtn).toBeVisible();
// Like should toggle to red and indicator appear immediately
const pressedBefore = await likeBtn.getAttribute('aria-pressed');
await likeBtn.click();
await expect.poll(async () => (await likeBtn.getAttribute('aria-pressed')) || '').toContain('true');
// Feedback indicator (title="Liked") should appear on the tile
await expect(tile.locator('[title="Liked"]')).toBeVisible();
// Open lightbox
await openBtn.click();
const closeLightboxBtn = gallery.getByRole('button', { name: /^Close$/i }).first();
await expect(closeLightboxBtn).toBeVisible();
// Close again to continue
await closeLightboxBtn.click();
// Comment quick action should open lightbox with feedback panel visible
await tile.hover({ force: true });
await commentBtn.click();
await expect(gallery.getByRole('button', { name: /Toggle feedback/ })).toBeVisible();
// Ensure feedback panel is visible or open it
const feedbackHeading = gallery.getByRole('heading', { name: /Photo Feedback/i });
if (!(await feedbackHeading.isVisible())) {
await gallery.getByRole('button', { name: /Toggle feedback/ }).click();
}
await expect(feedbackHeading).toBeVisible();
// Comments quick action should surface the feedback tools
const addCommentBtn = gallery.getByRole('button', { name: /Add Comment|Add comment/i });
await expect(addCommentBtn).toBeVisible();
await addCommentBtn.click();
// Allow UI to react without requiring text entry
await gallery.waitForTimeout(250);
// Close lightbox to continue (we do not submit to keep test idempotent)
await closeLightboxBtn.click();
// Download from tile should trigger a browser download event
await tile.hover({ force: true });
const downloadPromise = gallery.waitForEvent('download');
await downloadBtn.click();
const download = await downloadPromise;
expect((await download.path()) !== null).toBeTruthy();
}
});
});