Files
GerbilManager/gerbil-manager-web/e2e/feedback.spec.ts
Gulum ac06cadb23 feat(hilfe): Eigene Tickets — Fehlerberichte ansehen, bearbeiten, löschen, lösen
Unter Hilfe (/hilfe/tickets) eine Verwaltung der eingereichten „Fehler melden"-
Berichte: Liste (neueste zuerst) mit Status-Badge (Offen / ✓ Gelöst), Kontext-
Label, betroffenem Objekt, Nachricht und Datum. Pro Ticket: als gelöst markieren/
wieder öffnen, Nachricht inline bearbeiten, löschen (mit Bestätigung).

Backend: Feedback um Status ("Open"/"Resolved", Default Open) + nullable ResolvedAt
erweitert (weiterhin FK-frei → übersteht Ingest-Wipe); Migration AddFeedbackStatus.
Endpoints: GET /feedback (neueste zuerst, inkl. Status), PUT /feedback/{id}
(Nachricht/Status; leer→400, fehlt→404; lösen setzt/öffnen löscht ResolvedAt),
DELETE /feedback/{id} (fehlt→404).

Tests: FeedbackEndpointTests (CRUD + Status-Lebenszyklus + übersteht Ingest),
e2e tickets.spec.ts; dotnet 215, vitest 129, playwright grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:52:50 +02:00

130 lines
5.9 KiB
TypeScript

/** FEEDBACK: "Fehler melden" Dialog + Stammbaum-Rechtsklick ("ID kopieren" / "Fehler melden"). */
import { de, expect, skipUnlessMock, test } from './fixtures'
const f = de.feedback
const st = de.pages.stammbaum
test('Tierakte: "Fehler melden"-Button öffnet den Dialog, Senden zeigt Erfolg', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel')
// Button in den Aktionen öffnet den Dialog.
await page.getByRole('button', { name: f.button }).click()
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
await expect(dialog).toBeVisible()
// Debug-Kontext ist sichtbar (automatisch erfasst).
await expect(dialog).toContainText(f.debugTitle)
await expect(dialog).toContainText(f.contexts['gerbil-detail'])
// Beschreibung eintragen + senden.
await dialog.getByLabel(f.label).fill('Der Farbschlag stimmt nicht.')
await dialog.getByRole('button', { name: f.submit }).click()
// Erfolgs-Toast + Dialog schließt.
await expect(page.getByText(f.success)).toBeVisible()
await expect(dialog).not.toBeVisible()
// Der Bericht wurde mit Debug-Kontext gespeichert (zuletzt eingereichter Eintrag;
// die Mock-DB ist mit Beispiel-Tickets vorbefüllt).
expect(mockDb).not.toBeNull()
const report = mockDb!.feedback.find((r) => r.message === 'Der Farbschlag stimmt nicht.')
expect(report).toMatchObject({
message: 'Der Farbschlag stimmt nicht.',
context: 'gerbil-detail',
gerbilId: 'kruemel',
})
expect(report!.url).toContain('/rennmaeuse/kruemel')
expect(typeof report!.clientTimestamp).toBe('string')
})
test('Wurf-Ansicht: "Fehler melden"-Button öffnet den Dialog und sendet mit Wurf-Kontext', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/wuerfe/w-kruemel')
await page.getByRole('button', { name: f.button }).click()
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
await expect(dialog).toBeVisible()
await expect(dialog).toContainText(f.contexts['litter-detail'])
await dialog.getByLabel(f.label).fill('Die Wurfstärke ist falsch.')
await dialog.getByRole('button', { name: f.submit }).click()
await expect(page.getByText(f.success)).toBeVisible()
const report = mockDb!.feedback.find((r) => r.message === 'Die Wurfstärke ist falsch.')
expect(report).toMatchObject({ context: 'litter-detail', litterId: 'w-kruemel' })
})
test('Kontakt-Detail: "Fehler melden"-Button sendet mit Kontakt-Kontext', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/kontakte/con-meier')
await page.getByRole('button', { name: f.button }).click()
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
await expect(dialog).toBeVisible()
await expect(dialog).toContainText(f.contexts['contact-detail'])
await dialog.getByLabel(f.label).fill('Die Adresse stimmt nicht.')
await dialog.getByRole('button', { name: f.submit }).click()
await expect(page.getByText(f.success)).toBeVisible()
const report = mockDb!.feedback.find((r) => r.message === 'Die Adresse stimmt nicht.')
expect(report).toMatchObject({ context: 'contact-detail', contactId: 'con-meier' })
})
test('Stammbaum: Rechtsklick auf eine Karte zeigt "ID kopieren" + "Fehler melden"', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel/stammbaum')
await expect(page.locator('.pedigree-card').first()).toBeVisible()
// Rechtsklick auf die Wurzelkarte (Krümel).
await page.locator('.pedigree-card--root').click({ button: 'right' })
const menu = page.locator('.stammbaum-context-menu')
await expect(menu).toBeVisible()
await expect(menu.getByRole('menuitem', { name: st.contextMenu.copyId })).toBeVisible()
await expect(menu.getByRole('menuitem', { name: st.contextMenu.reportError })).toBeVisible()
// "Fehler melden" öffnet den Dialog mit Stammbaum-Kontext.
await menu.getByRole('menuitem', { name: st.contextMenu.reportError }).click()
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
await expect(dialog).toBeVisible()
await expect(dialog).toContainText(f.contexts.stammbaum)
await dialog.getByLabel(f.label).fill('Stammbaum-Bug.')
await dialog.getByRole('button', { name: f.submit }).click()
await expect(page.getByText(f.success)).toBeVisible()
const report = mockDb!.feedback.find((r) => r.message === 'Stammbaum-Bug.')
expect(report).toMatchObject({ context: 'stammbaum', gerbilId: 'kruemel' })
})
test('Stammbaum: langer Druck (Long-Press) öffnet dasselbe Kontextmenü', async ({ page }, testInfo) => {
skipUnlessMock()
// Der Long-Press-Code-Pfad (onTouchStart → Timer → Menü) ist projekt-unabhängig
// identisch; im Touch-emulierten phone-Projekt ist das Auslösen eines
// synthetischen TouchEvents unzuverlässig, daher hier auf das desktop-Projekt
// beschränkt (validiert dieselbe Logik).
testInfo.skip(testInfo.project.name === 'phone', 'synthetischer Long-Press im phone-Projekt instabil')
await page.goto('/rennmaeuse/kruemel/stammbaum')
const card = page.locator('.pedigree-card--root')
await expect(card).toBeVisible()
// Long-Press simulieren: echtes touchstart (mit Touch-Objekt) auslösen; der
// Timer im PedigreeCard öffnet nach ~500 ms das Menü.
const dispatched = await card.evaluate((el) => {
if (typeof Touch === 'undefined' || typeof TouchEvent === 'undefined') return false
const r = el.getBoundingClientRect()
const t = new Touch({ identifier: 1, target: el, clientX: r.left + 10, clientY: r.top + 10 })
el.dispatchEvent(
new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [t], targetTouches: [t], changedTouches: [t] }),
)
return true
})
test.skip(!dispatched, 'TouchEvent in diesem Projekt nicht verfügbar')
await expect(page.locator('.stammbaum-context-menu')).toBeVisible({ timeout: 2000 })
await expect(
page.locator('.stammbaum-context-menu').getByRole('menuitem', { name: st.contextMenu.copyId }),
).toBeVisible()
})