e8ea179228
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>
71 lines
2.9 KiB
TypeScript
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)
|
|
})
|