// Run against a built frontend with PLAYWRIGHT_MODULE pointing to an installed Playwright package. // Requests use isolated fixtures; no real account or application data is changed. import assert from 'node:assert/strict' import { readFile, writeFile } from 'node:fs/promises' const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || 'playwright') const base = process.env.EVENTS_FRONTEND_URL || 'http://127.0.0.1:3157' const browser = await chromium.launch({ headless: true }) const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) page.setDefaultTimeout(10000) const errors = [] const writes = [] const listRequests = [] const iconCollections = Object.fromEntries(await Promise.all(['heroicons', 'lucide'].map(async prefix => [ prefix, JSON.parse(await readFile(new URL(`../node_modules/@iconify/json/json/${prefix}.json`, import.meta.url), 'utf8')) ]))) let failList = false let allowCreate = true const project = { id: 42, name: 'Umbau Hafenstraße', projectNumber: 'P-2026-042' } const profile = { id: '11111111-1111-4111-8111-111111111111', full_name: 'Anna Beispiel', pinned_on_navigation: [], temp_config: {}, role: { rights: [] } } const tenant = { id: 1, name: 'Testbetrieb', extraModules: [], features: {}, locked: 'none' } let events = Array.from({ length: 26 }, (_, index) => ({ id: index + 1, name: index ? `Termin ${index + 1}` : 'Baubesprechung Hafen', startDate: `2026-10-${String(index + 1).padStart(2, '0')}T08:00:00.000Z`, endDate: index ? null : '2026-10-01T09:30:00.000Z', project: index ? null : project, state: index ? 'Final' : 'Entwurf', color: '#2563eb', quick: false, archived: false, profiles: null, repeatInterval: index ? 'Keine Wiederholung' : 'Wöchentlich' })) page.on('pageerror', error => errors.push(error.message)) await page.addInitScript(() => localStorage.setItem('token', 'fixture-only')) await page.route('**/*', async route => { const url = new URL(route.request().url()) if (url.hostname === 'api.iconify.design') { const collection = iconCollections[url.pathname.slice(1).replace('.json', '')] if (collection) return route.fulfill({ contentType: 'application/json', body: JSON.stringify(collection) }) } if (url.origin !== new URL(base).origin) return route.abort() if (url.pathname.startsWith('/api/_nuxt_icon/')) return route.continue() if (!url.pathname.startsWith('/api/') && !url.pathname.startsWith('/auth/')) return route.continue() let body = [] if (url.pathname === '/api/me') body = { user: { id: profile.id, email: 'anna@example.test', is_admin: false }, profile, tenants: [tenant], activeTenant: 1, permissions: ['events', 'projects', ...(allowCreate ? ['events-create'] : [])] } else if (url.pathname === '/api/tenant') body = tenant else if (url.pathname === '/api/tenant/profiles') body = { data: [profile] } else if (url.pathname === '/api/resource/projects') body = [project] else if (url.pathname === '/api/resource/events/paginated') { if (failList) return route.fulfill({ status: 503, contentType: 'application/json', body: '{"error":"Fixture unavailable"}' }) const search = (url.searchParams.get('search') || '').toLowerCase() const state = url.searchParams.get('filter[state]') const limit = Number(url.searchParams.get('limit')) const currentPage = Number(url.searchParams.get('page')) listRequests.push(Object.fromEntries(url.searchParams)) const matches = events.filter(event => !event.archived && (!state || state === event.state) && [event.name, event.project?.name, event.project?.projectNumber].some(value => String(value || '').toLowerCase().includes(search))) matches.sort((a, b) => url.searchParams.get('sort')?.endsWith(':asc') ? a.startDate.localeCompare(b.startDate) : b.startDate.localeCompare(a.startDate)) body = { data: matches.slice((currentPage - 1) * limit, currentPage * limit), queryConfig: { total: matches.length } } } else if (url.pathname === '/api/resource/events' && route.request().method() === 'POST') { const payload = route.request().postDataJSON() writes.push(payload) body = { id: 100, state: 'Final', repeatInterval: 'Keine Wiederholung', ...payload, archived: false } events.push({ ...body, project: payload.project === project.id ? project : null }) } else if (/^\/api\/resource\/events\/\d+(\/true)?$/.test(url.pathname)) { const event = events.find(item => item.id === Number(url.pathname.split('/')[4])) body = { ...event, project: event?.project?.id || null } if (route.request().method() === 'PUT') { Object.assign(event, route.request().postDataJSON()) writes.push(route.request().postDataJSON()) body = event } } await route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) }) }) const searchInput = () => page.getByRole('textbox', { name: 'Termine durchsuchen' }) const waitForList = () => page.getByRole('table').waitFor() try { await page.goto(`${base}/events`, { waitUntil: 'networkidle' }) await waitForList() assert.equal(await page.getByRole('columnheader').count(), 5) assert.equal(await page.locator('tbody tr').count(), 25) await page.getByRole('button', { name: 'Next Page', exact: true }).click() await page.getByRole('link', { name: 'Baubesprechung Hafen', exact: true }).first().waitFor() assert.equal(listRequests.at(-1).page, '2') await searchInput().fill('P-2026-042') await page.getByText('P-2026-042 · Umbau Hafenstraße', { exact: true }).first().waitFor() await waitForList() assert.equal(listRequests.at(-1).page, '1') assert.equal(await page.locator('tbody tr').count(), 1) await page.screenshot({ path: '/tmp/fedeo-events-desktop.png', fullPage: true }) await page.getByRole('combobox', { name: 'Status filtern' }).click() await page.getByRole('option', { name: 'Final', exact: true }).click() await page.getByRole('heading', { name: 'Keine passenden Termine' }).waitFor() await page.getByRole('button', { name: 'Filter zurücksetzen' }).click() await waitForList() await page.getByRole('link', { name: 'Neuer Termin', exact: true }).click() await page.getByRole('heading', { name: 'Termin erstellen' }).waitFor() await page.getByLabel('Name', { exact: true }).fill('Neu aus Terminübersicht') for (let index = 0; index < 2; index++) { await page.getByRole('button', { name: 'Datum auswählen', exact: true }).first().click() await page.getByRole('button', { name: 'Heute', exact: true }).click() await page.keyboard.press('Escape') } await page.getByLabel('Farbe auswählen', { exact: true }).fill('#f43f5e') const projectInputId = await page.locator('label').filter({ hasText: /^Projekt$/ }).getAttribute('for') await page.locator(`[id="${projectInputId}"]`).click() await page.getByPlaceholder('Suche...', { exact: true }).fill('2026-042') await page.getByRole('option', { name: project.name, exact: true }).click() await page.getByRole('button', { name: 'Erstellen', exact: true }).click() await page.waitForURL(`${base}/events`) await searchInput().fill('Neu aus Terminübersicht') await page.getByRole('link', { name: 'Neu aus Terminübersicht', exact: true }).first().waitFor() assert.equal(writes[0].project, 42) assert.equal(writes[0].color, '#f43f5e') assert.ok(writes[0].startDate && writes[0].endDate) await page.getByRole('link', { name: 'Neu aus Terminübersicht', exact: true }).first().click() await page.getByRole('heading', { name: 'Termin bearbeiten' }).waitFor() await page.getByLabel('Name', { exact: true }).fill('Bearbeiteter Termin') await page.getByRole('button', { name: 'Speichern', exact: true }).click() await page.waitForURL(`${base}/events`) await waitForList() await page.setViewportSize({ width: 390, height: 844 }) await searchInput().fill('P-2026-042') await page.getByRole('heading', { name: 'Baubesprechung Hafen' }).waitFor() assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false) await page.getByRole('link', { name: 'Neuer Termin', exact: true }).waitFor() await page.screenshot({ path: '/tmp/fedeo-events-mobile.png', fullPage: true }) await page.setViewportSize({ width: 1440, height: 1000 }) await page.goto(`${base}/standardEntity/events`, { waitUntil: 'networkidle' }) await searchInput().waitFor() failList = true await page.getByRole('button', { name: 'Termine aktualisieren' }).click() await page.getByText('Laden fehlgeschlagen', { exact: true }).waitFor() failList = false await page.getByRole('button', { name: 'Erneut versuchen' }).click() await waitForList() events = [] await page.getByRole('button', { name: 'Termine aktualisieren' }).click() await page.getByRole('heading', { name: 'Noch keine Termine' }).waitFor() assert.equal(await page.getByRole('link', { name: 'Neuer Termin', exact: true }).count(), 2) allowCreate = false await page.reload({ waitUntil: 'networkidle' }) await page.getByRole('heading', { name: 'Noch keine Termine' }).waitFor() assert.equal(await page.getByRole('link', { name: 'Neuer Termin', exact: true }).count(), 0) assert.deepEqual(errors, [], 'Browser runtime errors') console.log('Events browser checks passed: pagination, project-number search, status filter, create and edit with return to list, mobile layout, legacy URL, errors, empty state and create permission.') } catch (error) { await writeFile('/tmp/fedeo-events-error.html', await page.content()) await page.screenshot({ path: '/tmp/fedeo-events-error.png', fullPage: true }) throw error } finally { await browser.close() }