Files
breakpilot-compliance/admin-compliance/e2e/specs/compliance-advisor-widget.spec.ts
T
Benjamin Admin e8ea179228 feat(advisor): topic threads, per-question delete/copy, fullscreen
Adds case management to the Compliance Advisor widget.

- topic threads: cases group into threads; the left menu shows each
  thread's first question as the Thema with expandable follow-ups.
  Send = follow-up to the active thread (carries the thread's prior Q&A
  as history for contextual answers); "+" starts a new topic.
- delete: a trash action per question (menu + stacked view).
- copy: single Q&A (question + answer + evidence + footnotes) or a whole
  thread, as Markdown to the clipboard (pure formatters in copy.ts).
- fullscreen: compact -> panel -> fullscreen view.
- route.ts consumes an optional bounded `history` so follow-ups are
  contextual for both the widget and the workspace consumer.

Tests: copy formatter unit tests + Playwright specs (threads/new-topic,
delete, fullscreen, copy affordance). No deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 18:51:17 +02:00

71 lines
2.9 KiB
TypeScript

/**
* E2E: Compliance Advisor widget UX — topic threads, new-topic vs follow-up, delete, copy, fullscreen.
* Stubs the chat endpoint with an answer fixture so every ask yields a finished case.
*/
import { test, expect } from '../fixtures/sdk-fixtures'
const CHAT_ROUTE = '**/api/sdk/compliance-advisor/chat'
const openAdvisor = 'Compliance Advisor oeffnen'
const ANSWER = {
mode: 'answer',
question: '',
clarity: { is_underspecified: false, dominant_context: 'cyber', concentration: 0.9 },
general_answer: null,
answer: 'Musterantwort [1].',
scoped_query: null,
evidence: [{ evidence_id: 'e1', document: 'DSGVO', section: 'Art. 5', bindingness: 'binding' }],
citations: [{ citation_id: 'c1', number: 1, evidence_id: 'e1', document: 'DSGVO', section: 'Art. 5' }],
visual_evidence: [],
footnotes: [],
}
async function stub(page: import('@playwright/test').Page) {
await page.route(CHAT_ROUTE, (r) =>
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ANSWER) }),
)
}
test('new topic creates a second thread; copy control + fullscreen available', async ({ sdkPage }) => {
await stub(sdkPage)
await sdkPage.getByRole('button', { name: openAdvisor }).click()
const input = sdkPage.getByPlaceholder('Frage eingeben...')
await input.fill('Erste Frage')
await input.press('Enter')
await expect(sdkPage.getByText(/Musterantwort/)).toBeVisible()
// expand -> the topic tree ("Themen") appears in the left menu
await sdkPage.getByRole('button', { name: 'Vergroessern' }).click()
await expect(sdkPage.getByText('Themen')).toBeVisible()
await expect(sdkPage.getByText('Erste Frage').first()).toBeVisible()
// a second, separate topic
await sdkPage.getByPlaceholder('Folgefrage eingeben...').fill('Zweites Thema')
await sdkPage.getByRole('button', { name: 'Neues Thema' }).click()
await expect(sdkPage.getByText('Zweites Thema').first()).toBeVisible()
await expect(sdkPage.getByText('Erste Frage').first()).toBeVisible()
// copy affordance + fullscreen toggle
await expect(sdkPage.getByRole('button', { name: 'Diese Frage kopieren' }).first()).toBeVisible()
await sdkPage.getByRole('button', { name: 'Vollbild' }).click()
await expect(sdkPage.getByRole('button', { name: 'Vollbild verlassen' })).toBeVisible()
})
test('delete removes a question from the thread', async ({ sdkPage }) => {
await stub(sdkPage)
await sdkPage.getByRole('button', { name: openAdvisor }).click()
const input = sdkPage.getByPlaceholder('Frage eingeben...')
await input.fill('Zu löschen')
await input.press('Enter')
await expect(sdkPage.getByText(/Musterantwort/)).toBeVisible()
await sdkPage.getByRole('button', { name: 'Vergroessern' }).click()
await expect(sdkPage.getByText('Zu löschen').first()).toBeVisible()
await sdkPage.getByRole('button', { name: 'Frage löschen' }).first().click()
await expect(sdkPage.getByText('Zu löschen')).toHaveCount(0)
})