/** * FEEDBACK-TICKETS: "Meine Tickets" — Verwaltung der eingereichten Fehlerberichte. * Erreichbar über Hilfe → „Meine Tickets". Drei gefilterte Verlinkungen (Filter-Chips): * „Rückfragen" (im Dialog), „Offen", „Geschlossen" (zeigt den Changelog/fixNote). * Pro Ticket: gelöst markieren / wieder öffnen, Nachricht bearbeiten, antworten, löschen, * Frage/Antwort-Verlauf (thread). Das interne `agentContext` wird NIE gerendert. */ import { de, expect, gotoSection, skipUnlessMock, acceptNextDialog, test } from './fixtures' const tt = de.feedback.tickets /** In die gewünschte gefilterte Ansicht wechseln (Filter-Chip klicken). */ async function selectView(page: import('@playwright/test').Page, label: string) { await page.locator('.tickets-filter').filter({ hasText: label }).click() } test.describe('Meine Tickets', () => { test.beforeEach(() => skipUnlessMock()) test('über Hilfe erreichbar, Default-Ansicht „Offen" zeigt offenes Ticket mit Badge', async ({ page, }) => { await gotoSection(page, de.nav.help) await page.getByRole('link', { name: tt.link }).click() await expect(page.getByRole('heading', { name: tt.title })).toBeVisible() // Default = „Offen": offenes Ticket + dessen Nachricht sichtbar. await expect(page.getByText('Das Gewicht wird auf der Verlaufskurve falsch gerundet.')).toBeVisible() await expect(page.getByText(tt.statusOpen, { exact: true }).first()).toBeVisible() }) test('drei gefilterte Verlinkungen filtern korrekt, Anzahl-Badges stimmen', async ({ page }) => { await page.goto('/hilfe/tickets') // Filter-Chips vorhanden, jeweils mit Anzahl-Badge (1 pro Gruppe in den Seed-Daten). const dialog = page.locator('.tickets-filter').filter({ hasText: tt.filters.dialog }) const open = page.locator('.tickets-filter').filter({ hasText: tt.filters.open }) const closed = page.locator('.tickets-filter').filter({ hasText: tt.filters.closed }) await expect(dialog.locator('.tickets-filter__count')).toHaveText('1') // „Offen" = Open + Answered → das offene + das beantwortete Ticket = 2. await expect(open.locator('.tickets-filter__count')).toHaveText('2') await expect(closed.locator('.tickets-filter__count')).toHaveText('1') // „Offen": nur das offene Ticket. await expect(page.getByText('Das Gewicht wird auf der Verlaufskurve falsch gerundet.')).toBeVisible() await expect(page.getByText('Der Wurf B hat ein falsches Geburtsdatum.')).toHaveCount(0) await expect(page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.')).toHaveCount(0) // „Rückfragen": nur das Dialog-Ticket (NeedsInfo). await selectView(page, tt.filters.dialog) await expect(page.getByText('Der Wurf B hat ein falsches Geburtsdatum.')).toBeVisible() await expect(page.getByText('Das Gewicht wird auf der Verlaufskurve falsch gerundet.')).toHaveCount(0) // „Geschlossen": nur das gelöste Ticket. await selectView(page, tt.filters.closed) await expect(page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.')).toBeVisible() await expect(page.getByText('Der Wurf B hat ein falsches Geburtsdatum.')).toHaveCount(0) }) test('geschlossenes Ticket zeigt den Changelog (fixNote) + Datum', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(card.getByText(tt.changelogLabel)).toBeVisible() await expect( card.getByText('Der Vater von Krümel war falsch — er ist jetzt Balu. Bitte den Stammbaum neu laden.'), ).toBeVisible() // Datum „Gelöst am" in der Meta-Leiste. await expect(card.getByText(tt.resolvedLabel)).toBeVisible() }) test('agentContext wird NIE in der Züchterin-UI gerendert (in allen Ansichten)', async ({ page, }) => { await page.goto('/hilfe/tickets') // In jeder Ansicht prüfen: der interne Kontext-Text taucht nirgends auf. for (const view of [tt.filters.open, tt.filters.dialog, tt.filters.closed]) { await selectView(page, view) await expect(page.getByText('GEHEIM-AGENT-CONTEXT', { exact: false })).toHaveCount(0) } }) test('Verlauf (thread) früherer Runden wird angezeigt', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.dialog) const card = page.locator('.ticket-card').filter({ hasText: 'Wurf B' }) await expect(card.getByText(tt.threadLabel)).toBeVisible() // Frühere Runde (Frage + Antwort) sichtbar. await expect(card.getByText('Wurde der Wurf im Mai oder Juni geboren?')).toBeVisible() await expect(card.getByText('Im Juni.')).toBeVisible() // Aktuelle offene Rückfrage ebenfalls sichtbar. await expect(card.getByText('Welches Geburtsdatum ist korrekt')).toBeVisible() }) test('Ticket als gelöst markieren', async ({ page }) => { await page.goto('/hilfe/tickets') const openCard = page .locator('.ticket-card') .filter({ hasText: 'Das Gewicht wird auf der Verlaufskurve falsch gerundet.' }) await expect(openCard.getByText(tt.statusOpen, { exact: true })).toBeVisible() await openCard.getByRole('button', { name: tt.markResolved }).click() // Das Ticket verlässt die „Offen"-Ansicht — die Ansicht weicht automatisch auf // eine nicht-leere Gruppe aus. Das gelöste Ticket findet sich unter „Geschlossen". await selectView(page, tt.filters.closed) const resolvedCard = page .locator('.ticket-card') .filter({ hasText: 'Das Gewicht wird auf der Verlaufskurve falsch gerundet.' }) await expect(resolvedCard.locator('.ticket-badge--resolved')).toBeVisible() await expect(resolvedCard.getByRole('button', { name: tt.reopen })).toBeVisible() }) test('Nachricht eines Tickets bearbeiten', async ({ page }) => { await page.goto('/hilfe/tickets') // „Offen"-Ansicht (Default): das offene Ticket über den Datensatz-Namen adressieren. const card = page.locator('.ticket-card').filter({ hasText: 'Fridolin' }) await expect(card.getByText('Das Gewicht wird auf der Verlaufskurve falsch gerundet.')).toBeVisible() await card.getByRole('button', { name: tt.edit }).click() const textarea = card.locator('textarea') await textarea.fill('Korrigierte Beschreibung des Fehlers.') await card.getByRole('button', { name: tt.save }).click() await expect(page.getByText('Korrigierte Beschreibung des Fehlers.')).toBeVisible() }) test('beantwortetes Ticket: Erst-Text gesperrt, nur die neueste Antwort ist änderbar', async ({ page, }) => { await page.goto('/hilfe/tickets') // „Offen"-Ansicht (Default) zeigt auch beantwortete Tickets (Answered). const card = page.locator('.ticket-card').filter({ hasText: 'Karlsson ist Blau' }) await expect(card).toBeVisible() // Erst-Text ist gesperrt: kein „Bearbeiten"-Button mehr, sobald es eine Antwort gibt. await expect(card.getByRole('button', { name: tt.edit })).toHaveCount(0) // Die neueste Antwort ist dagegen noch änderbar (KI hat noch nicht reagiert → Answered). await expect(card.getByRole('button', { name: tt.amendAnswer })).toBeVisible() }) test('Rückfrage beantworten: Züchterin sieht Frage, antwortet, Badge wird „Beantwortet"', async ({ page, }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.dialog) const card = page.locator('.ticket-card').filter({ hasText: 'Wurf B' }) await expect(card).toBeVisible() // Rückfrage ist sichtbar; Status-Badge „Rückfrage offen". await expect(card.getByText('Welches Geburtsdatum ist korrekt')).toBeVisible() await expect(card.getByText(tt.statusNeedsInfo, { exact: true })).toBeVisible() // Antworten. await card.locator('.ticket-card__answer-form textarea').fill('Korrekt ist der 2. Juni.') await card.getByRole('button', { name: tt.answerButton }).click() // Antwort wird angezeigt + Badge wechselt auf „Beantwortet". Das Ticket verlässt die // Rückfragen-Liste und steht jetzt unter „Offen" (Ansicht wechselt automatisch dorthin, // da die Rückfragen-Gruppe nun leer ist). await expect(card.getByText('Korrekt ist der 2. Juni.')).toBeVisible() await expect(card.locator('.ticket-badge--answered')).toBeVisible() await expect(card.getByText(tt.statusAnswered, { exact: true })).toBeVisible() const openFilter = page.locator('.tickets-filter').filter({ hasText: tt.filters.open }) await expect(openFilter).toHaveClass(/tickets-filter--active/) }) test('Rückfrage rendert Markdown und ist (offen) nicht bearbeitbar', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.dialog) const card = page.locator('.ticket-card').filter({ hasText: 'Wurf B' }) await expect(card).toBeVisible() // Markdown: **fett** → , „- "-Aufzählung →
  • . await expect(card.locator('.ticket-card__question-text strong').first()).toBeVisible() await expect(card.locator('.ticket-card__question-text li')).toHaveCount(2) // Offene Rückfrage (NeedsInfo): kein „Bearbeiten"-Button, dafür ein Antwort-Feld. await expect(card.getByRole('button', { name: tt.edit })).toHaveCount(0) await expect(card.locator('.ticket-card__answer-form textarea')).toBeVisible() }) test('Ticket löschen = Soft-Delete: wandert in den Papierkorb, mit Countdown + Wiederherstellen', async ({ page, }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(card).toBeVisible() acceptNextDialog(page) await card.getByRole('button', { name: tt.delete }).click() // Aus „Geschlossen" verschwunden … await expect( page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), ).toHaveCount(0) // … aber im Papierkorb („Gelöscht") vorhanden, mit Countdown + Wiederherstellen-Button. await selectView(page, tt.filters.deleted) const trashed = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(trashed).toBeVisible() await expect(trashed.locator('.ticket-card__trash-notice')).toBeVisible() await expect(trashed.getByText('endgültig gelöscht')).toBeVisible() // Wiederherstellen → zurück aus dem Papierkorb (kehrt in „Geschlossen" zurück). await trashed.getByRole('button', { name: tt.restore }).click() await expect( page.locator('.ticket-card').filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }), ).toHaveCount(0) await selectView(page, tt.filters.closed) await expect( page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), ).toBeVisible() }) test('Wiederöffnen eines gelösten Tickets ohne Rückfrage → Rückfragen + Bitte um Infos + Zeitstempel', async ({ page, }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) // „Krümel"-Ticket ist Resolved und hatte KEINE Rückfrage (question === null). const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await card.getByRole('button', { name: tt.reopen }).click() // Landet in den „Rückfragen" mit der Bitte um mehr Infos + „Wieder geöffnet am". await selectView(page, tt.filters.dialog) const reopened = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(reopened.locator('.ticket-badge--needsinfo')).toBeVisible() await expect(reopened.getByText(tt.reopenRequest)).toBeVisible() await expect(reopened.getByText(tt.reopenedLabel)).toBeVisible() }) test('Volltextsuche filtert die Ticket-Liste', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) const search = page.getByPlaceholder(tt.searchPlaceholder) await search.fill('Krümel') await expect( page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), ).toBeVisible() await search.fill('xyzgibtsnicht') await expect(page.getByText(tt.emptySearch)).toBeVisible() }) test('Kategorie-Filter zeigt nur Tickets der gewählten Kategorie', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) // Das Krümel-Ticket hat die Kategorie „Stammbaum" → als Chip sichtbar. const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(card.locator('.ticket-card__category')).toHaveText('Stammbaum') // Über das Kategorie-Menü filtern. await page.getByLabel(tt.categoryFilterLabel).selectOption('Stammbaum') await expect( page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), ).toBeVisible() }) test('👍 auf gelöstem Ticket speichert Rückmeldung; 👎 öffnet es wieder', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(card.getByText(tt.helpfulQuestion)).toBeVisible() // 👍 → Dank-Text, kein Wiederöffnen. await card.getByRole('button', { name: tt.helpfulYes }).click() await expect(card.getByText(tt.helpfulThanks)).toBeVisible() }) test('👎 auf gelöstem Ticket öffnet es als Rückfrage wieder', async ({ page }) => { await page.goto('/hilfe/tickets') await selectView(page, tt.filters.closed) const card = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await card.getByRole('button', { name: tt.helpfulNo }).click() // Verlässt „Geschlossen", landet in „Rückfragen" mit Bitte um Infos. await selectView(page, tt.filters.dialog) const reopened = page .locator('.ticket-card') .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) await expect(reopened.locator('.ticket-badge--needsinfo')).toBeVisible() await expect(reopened.getByText(tt.helpfulReopenNote)).toBeVisible() }) test('Foto an ein Ticket anhängen erscheint als Vorschaubild', async ({ page }) => { await page.goto('/hilfe/tickets') // Offenes Ticket (Fridolin) — Anhang-Bereich mit „Foto anhängen". const card = page.locator('.ticket-card').filter({ hasText: 'Fridolin' }) await expect(card.getByText(tt.attachmentsLabel, { exact: true })).toBeVisible() // 1×1-PNG als Datei hochladen. const png = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64', ) await card.locator('input[type="file"]').setInputFiles({ name: 'maus.png', mimeType: 'image/png', buffer: png, }) // Nach dem Upload erscheint ein Vorschaubild im Anhang-Raster. await expect(card.locator('.ticket-card__attachment img')).toHaveCount(1) }) })