Files
fotospiel-app/tests/ui/guest/guest-limit-experience.test.ts
Codex Agent a35808ac15
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Update Playwright staging flows and Paddle sandbox checkout
2026-01-03 17:06:01 +01:00

272 lines
8.0 KiB
TypeScript

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.getByRole('button', { name: /Foto aufnehmen/i })).toBeVisible();
await expect(page.getByText(/Upload-Limit erreicht/i)).toHaveCount(0);
await page.goto(`/e/${EVENT_TOKEN}/gallery`);
await expect(page.getByRole('heading', { name: /Galerie/i }).first()).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 expect(page.getByRole('button', { name: /Foto aufnehmen/i })).toHaveCount(0);
await page.goto(`/e/${EVENT_TOKEN}/gallery`);
await expect(page.getByRole('heading', { name: /Galerie/i }).first()).toBeVisible();
});
test('blocks uploads and guest access once all limits are exhausted', async ({ page }) => {
await page.route(`**/api/v1/events/${EVENT_TOKEN}/package`, async (route) => {
const exhaustedAt = new Date().toISOString();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 44,
event_id: 1,
used_photos: 120,
expires_at: exhaustedAt,
package: {
id: 90,
name: 'Starter',
max_photos: 120,
max_guests: 3,
gallery_days: 30,
},
limits: {
photos: {
limit: 120,
used: 120,
remaining: 0,
percentage: 100,
state: 'limit_reached',
threshold_reached: 120,
next_threshold: null,
thresholds: [80, 95, 120],
},
guests: {
limit: 3,
used: 3,
remaining: 0,
percentage: 100,
state: 'limit_reached',
threshold_reached: 3,
next_threshold: null,
thresholds: [2, 3],
},
gallery: {
state: 'ok',
expires_at: exhaustedAt,
days_remaining: 10,
warning_thresholds: [7, 1],
warning_triggered: null,
warning_sent_at: null,
expired_notified_at: null,
},
can_upload_photos: false,
can_add_guests: false,
},
}),
});
});
await page.goto(`/e/${EVENT_TOKEN}/upload?task=1`);
await expect(page.getByText(/Upload-Limit erreicht/i)).toBeVisible();
await expect(page.getByRole('button', { name: /Foto aufnehmen/i })).toHaveCount(0);
await page.goto(`/e/${EVENT_TOKEN}/gallery`);
await expect(page.getByRole('heading', { name: /Galerie/i }).first()).toBeVisible();
});
});