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
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:
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user