mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 23:46:42 +02:00
Add an "eventItems" mini-collection attached to each social event so hosts can track what each guest is bringing, and so public visitors on the share-link page can claim an item without an account. Local-first side - New eventItems table (Dexie v11), module config update for sync. - LocalEventItem type + EventItem domain type, useEventItems query. - eventItemsStore: addItem / updateItem / toggleDone / assign / deleteItem. Every mutation pushes the full list to the server snapshot via eventsStore.syncItems if the event is published. - BringListEditor component on the host DetailView with assign-to- guest dropdown, quantity, and done-checkbox. - eventsStore.syncItems + a syncItems call in publishEvent so the public page sees pre-existing items as soon as the event ships. Server side - New event_items_published table (FK cascade from events_published so unpublishing wipes the bring list along with the snapshot). - Host endpoints PUT/GET /events/:eventId/items: full-replace upsert that preserves any existing claimed_by_name across host edits, max 100 items, ownership check. - Public POST /rsvp/:token/items/:itemId/claim: name-only claim, 1× per item (first write wins), shares the per-token hourly rate bucket with RSVP submissions to keep the abuse surface uniform. - GET /rsvp/:token now also returns the bring list (sorted) so the public page renders in a single round-trip. Public RSVP page - Renders the bring list with claim buttons; clicking prompts for a name and POSTs the claim, then optimistically updates the UI. - New bring-list i18n keys for all five locales (de/en/it/fr/es). Tests - 15 new server tests covering host PUT/GET (insert / update / prune / ownership / claimed-name preservation / cascade), GET /rsvp item exposure, and POST /claim (success / double-claim / cross-token / cancelled / validation). 50 server tests total, all green. - E2E spec scoped to .guest-editor where the new BringListEditor introduced a duplicate "Hinzufügen" button label.
110 lines
4.5 KiB
TypeScript
110 lines
4.5 KiB
TypeScript
/**
|
|
* Events module — end-to-end smoke test.
|
|
*
|
|
* Covers the local-first happy path (guest mode, no login):
|
|
* 1. Open /events on a fresh IndexedDB
|
|
* 2. Create an event via the inline form
|
|
* 3. Open the detail view
|
|
* 4. Add a guest, change RSVP status, see the summary update
|
|
* 5. Delete the event
|
|
*
|
|
* The "publish + public RSVP" flow is exercised separately in
|
|
* events-public-rsvp.spec.ts so we don't need a real auth dance here.
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { dismissWelcomeModal } from './helpers';
|
|
|
|
// Each test gets its own browser context so IndexedDB starts empty.
|
|
test.describe('Events module — local flow', () => {
|
|
test('create, edit guest list, delete an event in guest mode', async ({ page }) => {
|
|
// 1. Land on /events
|
|
await page.goto('/events', { waitUntil: 'networkidle' });
|
|
|
|
// AuthGate may show a guest-welcome modal — dismiss it if present.
|
|
await dismissWelcomeModal(page);
|
|
|
|
// Heading should appear
|
|
await expect(page.getByRole('heading', { name: 'Events' })).toBeVisible();
|
|
|
|
// 2. Create event via the inline form
|
|
await page.getByRole('button', { name: '+ Neues Event' }).click();
|
|
|
|
const title = `E2E Test Party ${Date.now()}`;
|
|
await page.getByPlaceholder("Worum geht's?", { exact: false }).fill(title);
|
|
|
|
// Date input — use a fixed future date
|
|
const future = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
const dateValue = future.toISOString().slice(0, 10);
|
|
await page.locator('input[type="date"]').fill(dateValue);
|
|
|
|
// Time input is pre-filled to 19:00, leave it
|
|
await page.getByPlaceholder('Ort (optional)').fill('Café am See');
|
|
await page.getByRole('button', { name: 'Event anlegen' }).click();
|
|
|
|
// After create, ListView callback navigates to /events/{id}
|
|
await page.waitForURL(/\/events\/[a-f0-9-]+$/);
|
|
|
|
// 3. Detail view should show the title
|
|
await expect(page.getByRole('heading', { name: title })).toBeVisible();
|
|
await expect(page.getByText('Café am See')).toBeVisible();
|
|
|
|
// 4. Add a guest — scope to the guest editor since the bring-list
|
|
// editor below it uses the same "Hinzufügen" button text.
|
|
const guestEditor = page.locator('.guest-editor');
|
|
await guestEditor.getByPlaceholder('Name', { exact: false }).fill('Tante Erika');
|
|
await guestEditor.getByPlaceholder(/E-Mail/i).fill('erika@example.com');
|
|
await guestEditor.getByRole('button', { name: 'Hinzufügen' }).click();
|
|
|
|
// Guest row should appear
|
|
await expect(page.getByText('Tante Erika')).toBeVisible();
|
|
await expect(page.getByText('erika@example.com')).toBeVisible();
|
|
|
|
// Set Erika to "Ja"
|
|
const rsvpSelect = page.locator('select.rsvp-select').first();
|
|
await rsvpSelect.selectOption('yes');
|
|
|
|
// Summary should reflect 1 yes / 1 attending (no plus-ones)
|
|
await expect(page.locator('.rsvp-summary .badge.yes .count')).toHaveText('1');
|
|
|
|
// Set 2 plus-ones (scoped to the guest editor — bring list also has a number input)
|
|
const plusOnes = guestEditor.locator('input[type="number"]').first();
|
|
await plusOnes.fill('2');
|
|
await plusOnes.blur();
|
|
|
|
// totalAttending should now be 3 (1 + 2 plus-ones)
|
|
await expect(page.locator('.rsvp-summary .total strong')).toHaveText('3');
|
|
|
|
// 5. Delete the event — confirm dialog
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await page.getByRole('button', { name: 'Löschen' }).click();
|
|
|
|
// We should land back on the events list
|
|
await page.waitForURL(/\/events\/?$/);
|
|
await expect(page.getByRole('heading', { name: 'Events' })).toBeVisible();
|
|
|
|
// The deleted event should no longer appear
|
|
await expect(page.getByText(title)).not.toBeVisible();
|
|
});
|
|
|
|
test('quick-input adapter creates an event from the title bar', async ({ page }) => {
|
|
await page.goto('/events', { waitUntil: 'networkidle' });
|
|
|
|
await dismissWelcomeModal(page);
|
|
|
|
// Sanity: page loaded
|
|
await expect(page.getByRole('heading', { name: 'Events' })).toBeVisible();
|
|
|
|
// Create via the visible "+ Neues Event" form (we don't depend on the
|
|
// global QuickInputBar here — it lives outside the events route).
|
|
await page.getByRole('button', { name: '+ Neues Event' }).click();
|
|
const title = `Quick ${Date.now()}`;
|
|
await page.getByPlaceholder("Worum geht's?", { exact: false }).fill(title);
|
|
const future = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
|
|
await page.locator('input[type="date"]').fill(future.toISOString().slice(0, 10));
|
|
await page.getByRole('button', { name: 'Event anlegen' }).click();
|
|
|
|
await page.waitForURL(/\/events\/[a-f0-9-]+$/);
|
|
await expect(page.getByRole('heading', { name: title })).toBeVisible();
|
|
});
|
|
});
|