- Feedback: fixNote (Changelog), agentContext (intern), thread (Q&A-Verlauf); 3 gefilterte Ansichten (Rückfragen/Offen/Geschlossen) - Tickets-Text: klickbare Tier-Links (ID→Name), Ticket→Ticket-Links (?focus=), Markdown-Links; Antwort-Entwurf pro Ticket in localStorage - "Fehler melden": Entwurf übersteht App-Wechsel/Schließen (ein gemeinsamer Entwurf) - entityName als Hyperlink zum referenzierten Datensatz Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
6.8 KiB
TypeScript
151 lines
6.8 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('Entwurf bleibt nach Schließen + Navigation + Wiederöffnen erhalten', async ({ page }) => {
|
|
skipUnlessMock()
|
|
// Auf einer Tier-Seite anfangen zu schreiben …
|
|
await page.goto('/rennmaeuse/kruemel')
|
|
await page.getByRole('button', { name: f.button }).click()
|
|
const dialog = page.getByRole('dialog', { name: f.dialogTitle })
|
|
await dialog.getByLabel(f.label).fill('Halb fertiger Text, will kurz was nachschauen …')
|
|
|
|
// … Fenster (versehentlich) schließen …
|
|
await dialog.getByRole('button', { name: f.close }).click()
|
|
await expect(dialog).not.toBeVisible()
|
|
|
|
// … in der App woanders hin navigieren (anderer Kontext) …
|
|
await page.goto('/wuerfe/w-kruemel')
|
|
|
|
// … „Fehler melden" erneut öffnen: der Text ist noch da.
|
|
await page.getByRole('button', { name: f.button }).click()
|
|
const dialog2 = page.getByRole('dialog', { name: f.dialogTitle })
|
|
await expect(dialog2.getByLabel(f.label)).toHaveValue('Halb fertiger Text, will kurz was nachschauen …')
|
|
})
|
|
|
|
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()
|
|
})
|