rework of the e2e test suites

This commit is contained in:
Codex Agent
2025-11-19 22:23:33 +01:00
parent 8d2075bdd2
commit 0127114e59
32 changed files with 1593 additions and 124 deletions

View File

@@ -0,0 +1,209 @@
import { test, expect } from '@playwright/test';
const EVENT_TOKEN = 'limit-event';
function nowIso(): string {
return new Date().toISOString();
}
test.describe('Guest PWA limit experiences', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(
({ token }) => {
try {
window.localStorage.setItem(`guestName_${token}`, 'Playwright Gast');
window.localStorage.setItem(`guestCameraPrimerDismissed_${token}`, '1');
} catch (error) {
console.warn('Failed to seed guest storage', error);
}
if (!navigator.mediaDevices) {
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: {},
});
}
navigator.mediaDevices.getUserMedia = () => Promise.resolve(new MediaStream());
},
{ token: EVENT_TOKEN }
);
const timestamp = nowIso();
await page.route(`**/api/v1/events/${EVENT_TOKEN}`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
slug: EVENT_TOKEN,
name: 'Limit Experience Event',
default_locale: 'de',
created_at: timestamp,
updated_at: timestamp,
}),
});
});
await page.route(`**/api/v1/events/${EVENT_TOKEN}/tasks`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 1,
title: 'Playwright Mission',
description: 'Test mission for upload limits',
instructions: 'Mach ein Testfoto',
duration: 2,
},
]),
});
});
await page.route(`**/api/v1/events/${EVENT_TOKEN}/stats`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
online_guests: 5,
tasks_solved: 12,
latest_photo_at: timestamp,
}),
});
});
await page.route(`**/api/v1/events/${EVENT_TOKEN}/photos**`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: 101,
file_path: '/photos/101.jpg',
thumbnail_path: '/photos/101-thumb.jpg',
created_at: timestamp,
likes_count: 3,
},
],
latest_photo_at: timestamp,
}),
});
});
});
test('shows limit warnings and countdown before limits are reached', async ({ page }) => {
const expiresAt = new Date(Date.now() + 2 * 86_400_000).toISOString();
await page.route(`**/api/v1/events/${EVENT_TOKEN}/package`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 42,
event_id: 1,
used_photos: 95,
expires_at: expiresAt,
package: {
id: 77,
name: 'Starter',
max_photos: 100,
max_guests: 150,
gallery_days: 30,
},
limits: {
photos: {
limit: 100,
used: 95,
remaining: 5,
percentage: 95,
state: 'warning',
threshold_reached: 95,
next_threshold: 100,
thresholds: [80, 95, 100],
},
guests: null,
gallery: {
state: 'warning',
expires_at: expiresAt,
days_remaining: 2,
warning_thresholds: [7, 1],
warning_triggered: 2,
warning_sent_at: null,
expired_notified_at: null,
},
can_upload_photos: true,
can_add_guests: true,
},
}),
});
});
await page.goto(`/e/${EVENT_TOKEN}/upload?task=1`);
await expect(page.getByText(/Nur noch 5 von 100 Fotos möglich/i)).toBeVisible();
await expect(page.getByText(/Galerie läuft in 2 Tagen ab/i)).toBeVisible();
await page.goto(`/e/${EVENT_TOKEN}/gallery`);
await expect(page.getByText(/Noch 2 Tage online/i)).toBeVisible();
await expect(page.getByText(/Nur noch 5 von 100 Fotos möglich/i)).toBeVisible();
await expect(page.getByText(/Galerie läuft in 2 Tagen ab/i)).toBeVisible();
await expect(page.getByRole('button', { name: /Letzte Fotos hochladen/i })).toBeVisible();
});
test('marks uploads as blocked and highlights expired gallery state', async ({ page }) => {
const expiredAt = new Date(Date.now() - 86_400_000).toISOString();
await page.route(`**/api/v1/events/${EVENT_TOKEN}/package`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 43,
event_id: 1,
used_photos: 100,
expires_at: expiredAt,
package: {
id: 77,
name: 'Starter',
max_photos: 100,
max_guests: 150,
gallery_days: 30,
},
limits: {
photos: {
limit: 100,
used: 100,
remaining: 0,
percentage: 100,
state: 'limit_reached',
threshold_reached: 100,
next_threshold: null,
thresholds: [80, 95, 100],
},
guests: null,
gallery: {
state: 'expired',
expires_at: expiredAt,
days_remaining: 0,
warning_thresholds: [7, 1],
warning_triggered: 0,
warning_sent_at: null,
expired_notified_at: expiredAt,
},
can_upload_photos: false,
can_add_guests: true,
},
}),
});
});
await page.goto(`/e/${EVENT_TOKEN}/upload?task=1`);
await expect(page.getByText(/Upload-Limit erreicht/i)).toBeVisible();
await page.goto(`/e/${EVENT_TOKEN}/gallery`);
await expect(page.getByText(/Galerie abgelaufen/i)).toBeVisible();
await expect(page.getByText(/Die Galerie ist abgelaufen\. Uploads sind nicht mehr möglich\./i)).toBeVisible();
});
});

View File

@@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test';
test.describe('Guest Profile Flow', () => {
test('should require name setup on first event join and persist it', async ({ page }) => {
// Assume Vite dev server is running on localhost:5173
await page.goto('http://localhost:5173/');
// Enter event slug manually
await page.fill('input[placeholder*="Event-Code"]', 'test-event');
await page.click('button:has-text("Event beitreten")');
// Should redirect to setup if no name
await expect(page).toHaveURL(/.*\/e\/test-event\/setup/);
// Fill name and submit
await page.fill('input[placeholder*="Dein Name"]', 'Test User');
await page.click('button:has-text("LET\'S GO! ✨")');
// Should navigate to home
await expect(page).toHaveURL(/.*\/e\/test-event$/);
// Check localStorage
const storedName = await page.evaluate(() => localStorage.getItem('guestName_test-event'));
expect(storedName).toBe('Test User');
// Reload to test persistence - should stay on home, not redirect to setup
await page.reload();
await expect(page).toHaveURL(/.*\/e\/test-event$/);
// Re-nav to landing and join again - should go directly to home
await page.goto('http://localhost:5173/');
await page.fill('input[placeholder*="Event-Code"]', 'test-event');
await page.click('button:has-text("Event beitreten")');
await expect(page).toHaveURL(/.*\/e\/test-event$/);
});
test('should go directly to home if name already stored', async ({ page }) => {
// Pre-set name in localStorage
await page.addInitScript(() => {
localStorage.setItem('guestName_test-event', 'Existing User');
});
await page.goto('http://localhost:5173/');
// Join
await page.fill('input[placeholder*="Event-Code"]', 'test-event');
await page.click('button:has-text("Event beitreten")');
// Should go directly to home
await expect(page).toHaveURL(/.*\/e\/test-event$/);
});
});

View File

@@ -0,0 +1,89 @@
import fs from 'node:fs';
import path from 'node:path';
import { expectFixture as expect, test } from '../helpers/test-fixtures';
const guestCount = 15;
const uploadFixturePath = ensureUploadFixture();
test.describe('Guest PWA multi-guest journey', () => {
test('15 guests can onboard, explore tasks, trigger upload review, and reach gallery', async ({
browser,
fetchJoinToken,
}) => {
const eventSlug = process.env.E2E_GUEST_EVENT_SLUG;
test.skip(!eventSlug, 'Set E2E_GUEST_EVENT_SLUG to point the guest suite at an existing event.');
const joinToken = await fetchJoinToken({ slug: eventSlug!, ensureActive: true });
const baseUrl = (process.env.E2E_GUEST_BASE_URL ?? 'http://localhost:8000').replace(/\/+$/, '');
const landingUrl = `${baseUrl}/event`;
const eventBaseUrl = `${baseUrl}/e/${joinToken.token}`;
for (let index = 0; index < guestCount; index += 1) {
const context = await browser.newContext();
const page = await context.newPage();
const guestName = `Gast ${index + 1}`;
await page.goto(landingUrl, { waitUntil: 'domcontentloaded' });
await page.getByPlaceholder(/Event-Code eingeben|Enter event code/i).fill(joinToken.token);
await page.getByRole('button', { name: /Event beitreten|Join event/i }).click();
await completeProfileSetup(page, guestName, joinToken.token);
await page.goto(`${eventBaseUrl}/tasks`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('body')).toContainText(/Aufgaben|Tasks/);
await page.goto(`${eventBaseUrl}/upload`, { waitUntil: 'domcontentloaded' });
const fileInput = page.locator('input[type="file"]');
await expect(fileInput).toBeVisible({ timeout: 15_000 });
await fileInput.setInputFiles(uploadFixturePath);
await expect(
page.getByRole('button', { name: /Nochmal aufnehmen|Retake/i })
).toBeVisible({ timeout: 15_000 });
await page.getByRole('button', { name: /Nochmal aufnehmen|Retake/i }).click();
// Simulate offline queue testing for the last five guests.
if (index >= guestCount - 5) {
await context.setOffline(true);
await page.goto(`${eventBaseUrl}/tasks`, { waitUntil: 'domcontentloaded' }).catch(() => undefined);
await context.setOffline(false);
}
await page.goto(`${eventBaseUrl}/gallery`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('body')).toContainText(/Galerie|Gallery/);
const likeButtons = page.getByLabel(/Foto liken|Like photo/i);
if (await likeButtons.count()) {
await likeButtons.first().click();
}
await context.close();
}
});
});
async function completeProfileSetup(page: import('@playwright/test').Page, guestName: string, token: string) {
await page.waitForLoadState('domcontentloaded');
if (page.url().includes('/setup/')) {
await page.getByLabel(/Dein Name|Your name/i).fill(guestName);
await page.getByRole('button', { name: /Los gehts|Let's go/i }).click();
}
await page.waitForURL(new RegExp(`/e/${token}`), {
timeout: 60_000,
});
}
function ensureUploadFixture(): string {
const fixtureDir = path.join(process.cwd(), 'tests/ui/guest/fixtures');
const fixturePath = path.join(fixtureDir, 'sample-upload.png');
if (!fs.existsSync(fixtureDir)) {
fs.mkdirSync(fixtureDir, { recursive: true });
}
if (!fs.existsSync(fixturePath)) {
const png1x1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==',
'base64'
);
fs.writeFileSync(fixturePath, png1x1);
}
return fixturePath;
}