feat(events): restore responsive appointment overview and creation
This commit is contained in:
@@ -385,6 +385,8 @@ const getEntityModalCreateQuery = (datapoint) => {
|
||||
const getPostSaveRoute = () => {
|
||||
if (type !== "events") return null
|
||||
|
||||
if (route.query.returnTo === "events") return "/events"
|
||||
|
||||
if (route.query.returnTo === "plantafel") {
|
||||
return {
|
||||
path: "/organisation/plantafel",
|
||||
@@ -475,25 +477,21 @@ const updateItem = async () => {
|
||||
<template>
|
||||
<UDashboardNavbar
|
||||
v-if="!props.inModal"
|
||||
:title="item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen`"
|
||||
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
|
||||
>
|
||||
<template #toggle>
|
||||
<div v-if="platform === 'mobile'"></div>
|
||||
</template>
|
||||
<template #left>
|
||||
<template #leading>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
variant="outline"
|
||||
aria-label="Zurück"
|
||||
@click="router.back()"
|
||||
>
|
||||
</UButton>
|
||||
</template>
|
||||
<template #center>
|
||||
<h1
|
||||
v-if="item"
|
||||
:class="['text-xl','font-medium', 'text-center']"
|
||||
>{{ item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen` }}</h1>
|
||||
</template>
|
||||
<template #right>
|
||||
<ArchiveButton
|
||||
color="error"
|
||||
@@ -517,7 +515,7 @@ const updateItem = async () => {
|
||||
Erstellen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(item.id ? `/standardEntity/${type}/show/${item.id}` : `/standardEntity/${type}`)"
|
||||
@click="router.push(getPostSaveRoute() || (item.id ? `/standardEntity/${type}/show/${item.id}` : `/standardEntity/${type}`))"
|
||||
color="red"
|
||||
class="ml-1"
|
||||
>
|
||||
@@ -527,14 +525,9 @@ const updateItem = async () => {
|
||||
</UDashboardNavbar>
|
||||
<UDashboardNavbar
|
||||
v-else
|
||||
:title="props.floatingWindow ? '' : (item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen`)"
|
||||
:ui="{center: 'flex items-stretch gap-1.5 min-w-0'}"
|
||||
>
|
||||
<template #center>
|
||||
<h1
|
||||
v-if="item && !props.floatingWindow"
|
||||
:class="['text-xl', 'font-medium']"
|
||||
>{{ item.id ? `${dataType.labelSingle} bearbeiten` : `${dataType.labelSingle} erstellen` }}</h1>
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="item.id"
|
||||
|
||||
@@ -38,6 +38,12 @@ const isRouteActive = (to) => {
|
||||
|
||||
const links = computed(() => {
|
||||
const organisationChildren = [
|
||||
has("events") && featureEnabled("calendar") ? {
|
||||
label: "Termine",
|
||||
to: "/events",
|
||||
icon: "i-heroicons-calendar-days",
|
||||
active: route.path.startsWith("/standardEntity/events")
|
||||
} : null,
|
||||
has("tasks") && featureEnabled("tasks") ? {
|
||||
label: "Aufgaben",
|
||||
to: "/tasks",
|
||||
|
||||
206
frontend/pages/events/index.vue
Normal file
206
frontend/pages/events/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
definePageMeta({ alias: ['/standardEntity/events'] })
|
||||
useHead({ title: 'Termine · FEDEO' })
|
||||
|
||||
const auth = useAuthStore()
|
||||
const { has } = usePermission()
|
||||
const eventEntities = useEntities('events')
|
||||
const canView = computed(() => has('events') && auth.activeTenantData?.features?.calendar !== false)
|
||||
const canCreate = computed(() => canView.value && has('events-create'))
|
||||
const events = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const search = ref('')
|
||||
const status = ref('all')
|
||||
const page = ref(1)
|
||||
const pageSize = 25
|
||||
const sortDirection = ref('desc')
|
||||
const createRoute = { path: '/standardEntity/events/create', query: { returnTo: 'events' } }
|
||||
const statusOptions = [
|
||||
{ label: 'Alle Status', value: 'all' },
|
||||
{ label: 'Entwurf', value: 'Entwurf' },
|
||||
{ label: 'Final', value: 'Final' }
|
||||
]
|
||||
const sortOptions = [
|
||||
{ label: 'Beginn: neueste zuerst', value: 'desc' },
|
||||
{ label: 'Beginn: älteste zuerst', value: 'asc' }
|
||||
]
|
||||
const columns = [
|
||||
{ accessorKey: 'name', header: 'Termin' },
|
||||
{ accessorKey: 'startDate', header: 'Zeitraum' },
|
||||
{ accessorKey: 'project', header: 'Projekt' },
|
||||
{ accessorKey: 'state', header: 'Status' },
|
||||
{ id: 'actions', header: '' }
|
||||
]
|
||||
let requestVersion = 0
|
||||
let searchTimer
|
||||
|
||||
const editRoute = (event) => ({
|
||||
path: `/standardEntity/events/edit/${event.id}`,
|
||||
query: { returnTo: 'events' }
|
||||
})
|
||||
const eventTitle = (event) => event.name || event.project?.name || 'Termin ohne Titel'
|
||||
const projectLabel = (event) => {
|
||||
const project = event.project
|
||||
if (!project) return 'Ohne Projekt'
|
||||
if (typeof project !== 'object') return `Projekt ${project}`
|
||||
return [project.projectNumber, project.name].filter(Boolean).join(' · ') || 'Ohne Projekt'
|
||||
}
|
||||
const formatDate = (value) => value && dayjs(value).isValid() ? dayjs(value).format('DD.MM.YYYY HH:mm') : 'Nicht angegeben'
|
||||
const eventColor = (event) => event.color
|
||||
|| (event.quick ? auth.activeTenantData?.calendarConfig?.quickEntry?.color : null)
|
||||
|| auth.activeTenantData?.calendarConfig?.eventTypes?.find((type) => type.label === event.eventtype)?.color
|
||||
|| '#2563eb'
|
||||
|
||||
async function loadEvents() {
|
||||
const version = ++requestVersion
|
||||
if (!canView.value) {
|
||||
events.value = []
|
||||
total.value = 0
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const result = await eventEntities.selectPaginated({
|
||||
select: '*, project(id,name,projectNumber)',
|
||||
filters: { archived: false, ...(status.value === 'all' ? {} : { state: status.value }) },
|
||||
search: search.value.trim(),
|
||||
sort: [{ field: 'startDate', direction: sortDirection.value }],
|
||||
page: page.value,
|
||||
limit: pageSize
|
||||
})
|
||||
if (version !== requestVersion) return
|
||||
events.value = result.data
|
||||
total.value = result.meta?.total || 0
|
||||
} catch {
|
||||
if (version !== requestVersion) return
|
||||
events.value = []
|
||||
total.value = 0
|
||||
error.value = 'Die Termine konnten nicht geladen werden. Bitte versuche es erneut.'
|
||||
} finally {
|
||||
if (version === requestVersion) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reloadFromFirstPage() {
|
||||
clearTimeout(searchTimer)
|
||||
if (page.value !== 1) page.value = 1
|
||||
else loadEvents()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
search.value = ''
|
||||
status.value = 'all'
|
||||
}
|
||||
|
||||
watch(search, () => {
|
||||
++requestVersion
|
||||
loading.value = true
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(reloadFromFirstPage, 250)
|
||||
})
|
||||
watch([status, sortDirection, canView, () => auth.activeTenant], reloadFromFirstPage)
|
||||
watch(page, loadEvents)
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(searchTimer)
|
||||
++requestVersion
|
||||
})
|
||||
loadEvents()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar title="Termine" :badge="total">
|
||||
<template #right>
|
||||
<UButton v-if="canCreate" :to="createRoute" icon="i-heroicons-plus" label="Neuer Termin" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent class="space-y-4">
|
||||
<UAlert v-if="!canView" color="neutral" variant="subtle" title="Termine sind für deinen Zugang nicht verfügbar." />
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<UInput
|
||||
v-model="search"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Termin, Projekt oder Projektnummer suchen …"
|
||||
aria-label="Termine durchsuchen"
|
||||
class="w-full lg:flex-1"
|
||||
/>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<USelect v-model="status" :items="statusOptions" aria-label="Status filtern" class="min-w-36 flex-1 lg:flex-none" />
|
||||
<USelect v-model="sortDirection" :items="sortOptions" aria-label="Termine sortieren" class="min-w-48 flex-1 lg:flex-none" />
|
||||
<UButton color="neutral" variant="outline" icon="i-heroicons-arrow-path" aria-label="Termine aktualisieren" :loading="loading" @click="loadEvents" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert v-if="error" color="error" variant="subtle" title="Laden fehlgeschlagen" :description="error">
|
||||
<template #actions>
|
||||
<UButton color="error" variant="outline" label="Erneut versuchen" @click="loadEvents" />
|
||||
</template>
|
||||
</UAlert>
|
||||
<div v-else-if="loading" class="space-y-3 py-4" role="status" aria-label="Termine werden geladen">
|
||||
<USkeleton v-for="index in 5" :key="index" class="h-20 w-full rounded-lg" />
|
||||
</div>
|
||||
<div v-else-if="!events.length" class="rounded-xl border border-dashed border-default px-4 py-14 text-center">
|
||||
<UIcon name="i-heroicons-calendar-days" class="mx-auto mb-3 size-10 text-muted" />
|
||||
<h2 class="text-lg font-semibold">{{ search || status !== 'all' ? 'Keine passenden Termine' : 'Noch keine Termine' }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ search || status !== 'all' ? 'Passe deine Suche oder den Statusfilter an.' : 'Erstelle deinen ersten Termin direkt über diese Übersicht.' }}</p>
|
||||
<UButton v-if="search || status !== 'all'" class="mt-4" color="neutral" variant="outline" label="Filter zurücksetzen" @click="resetFilters" />
|
||||
<UButton v-else-if="canCreate" class="mt-4" :to="createRoute" icon="i-heroicons-plus" label="Neuer Termin" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<UTable :data="events" :columns="columns" class="hidden rounded-xl border border-default md:block" :ui="{ td: 'align-top' }">
|
||||
<template #name-cell="{ row }">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-1 size-3 shrink-0 rounded-full" :style="{ backgroundColor: eventColor(row.original) }" aria-hidden="true" />
|
||||
<div class="min-w-0">
|
||||
<NuxtLink :to="editRoute(row.original)" class="block max-w-sm whitespace-normal break-words font-medium text-highlighted hover:text-primary">{{ eventTitle(row.original) }}</NuxtLink>
|
||||
<p v-if="row.original.repeatInterval && row.original.repeatInterval !== 'Keine Wiederholung'" class="mt-1 text-xs text-muted">{{ row.original.repeatInterval }}</p>
|
||||
<p v-if="row.original.quick" class="mt-1 text-xs text-muted">Quick-Eintrag</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #startDate-cell="{ row }">
|
||||
<div>{{ formatDate(row.original.startDate) }}</div>
|
||||
<div v-if="row.original.endDate" class="mt-1 text-xs text-muted">bis {{ formatDate(row.original.endDate) }}</div>
|
||||
</template>
|
||||
<template #project-cell="{ row }">
|
||||
<span class="block max-w-xs whitespace-normal break-words">{{ projectLabel(row.original) }}</span>
|
||||
</template>
|
||||
<template #state-cell="{ row }">
|
||||
<UBadge :color="row.original.state === 'Entwurf' ? 'warning' : 'success'" variant="subtle">{{ row.original.state || 'Final' }}</UBadge>
|
||||
</template>
|
||||
<template #actions-cell="{ row }">
|
||||
<UButton :to="editRoute(row.original)" color="neutral" variant="ghost" icon="i-heroicons-pencil-square" :aria-label="`${eventTitle(row.original)} bearbeiten`" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<div class="space-y-3 md:hidden">
|
||||
<NuxtLink v-for="event in events" :key="event.id" :to="editRoute(event)" class="block rounded-xl border border-default p-4 hover:border-primary">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-1 size-3 shrink-0 rounded-full" :style="{ backgroundColor: eventColor(event) }" aria-hidden="true" />
|
||||
<h2 class="min-w-0 flex-1 break-words font-semibold text-highlighted">{{ eventTitle(event) }}</h2>
|
||||
<UBadge :color="event.state === 'Entwurf' ? 'warning' : 'success'" variant="subtle" class="shrink-0">{{ event.state || 'Final' }}</UBadge>
|
||||
</div>
|
||||
<p class="mt-3 text-sm">{{ formatDate(event.startDate) }}</p>
|
||||
<p v-if="event.endDate" class="text-sm text-muted">bis {{ formatDate(event.endDate) }}</p>
|
||||
<p class="mt-2 break-words text-sm text-muted">{{ projectLabel(event) }}</p>
|
||||
<p v-if="event.repeatInterval && event.repeatInterval !== 'Keine Wiederholung'" class="mt-2 text-xs text-muted">{{ event.repeatInterval }}</p>
|
||||
<p v-if="event.quick" class="mt-1 text-xs text-muted">Quick-Eintrag</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-default pt-4">
|
||||
<p class="text-sm text-muted">{{ total }} {{ total === 1 ? 'Termin' : 'Termine' }}</p>
|
||||
<UPagination v-model:page="page" :total="total" :items-per-page="pageSize" :sibling-count="0" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
166
frontend/tests/events.browser.mjs
Normal file
166
frontend/tests/events.browser.mjs
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user