QA-2: Abgabe-Wizard e2e (ForSale-Seed, sale-ad+contracts Mock, abgabe.spec.ts)
This commit is contained in:
127
gerbil-manager-web/e2e/abgabe.spec.ts
Normal file
127
gerbil-manager-web/e2e/abgabe.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* QA-2: Abgabe-Wizard (FEAT-12a + FEAT-13) — Abgabetiere gruppieren,
|
||||
* KI-Inserat generieren, und Vertrag-Handoff via ?tiere= Query-Parameter.
|
||||
*
|
||||
* Mock-gebunden (Seed: 2 ForSale-Tiere im Großbecken) → skipUnlessMock.
|
||||
*/
|
||||
import { acceptNextDialog, de, expect, gotoSection, skipUnlessMock, test } from './fixtures'
|
||||
|
||||
const ta = de.pages.abgabe
|
||||
const tw = de.pages.vertraege.wizard
|
||||
const tk = de.pages.kontakte
|
||||
|
||||
test.describe('Abgabe', () => {
|
||||
test('Abgabe-Seite zeigt ForSale-Tiere gruppiert nach Becken', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await gotoSection(page, de.nav.forSale)
|
||||
await expect(page.getByRole('heading', { name: ta.title, exact: true })).toBeVisible()
|
||||
await expect(page.getByText(ta.grouping.title)).toBeVisible()
|
||||
|
||||
// Die beiden ForSale-Tiere müssen sichtbar sein
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
await expect(page.getByText('Benny Abgabe')).toBeVisible()
|
||||
|
||||
// Inserat-Vorschau (listing-preview) wird gerendert
|
||||
await expect(page.locator('.listing-preview').first()).toBeVisible()
|
||||
// Status-Dropdown standard: FREI
|
||||
await expect(
|
||||
page.locator('label.field').filter({ hasText: ta.listing.status }).locator('select').first()
|
||||
).toHaveValue('free')
|
||||
})
|
||||
|
||||
test('KI-Inserat: "Text mit KI verbessern" — Mock liefert Inserat-Text', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await gotoSection(page, de.nav.forSale)
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: ta.ai.improve }).first().click()
|
||||
// Mock antwortet synchron; der generierte Text erscheint in der Vorschau
|
||||
await expect(page.locator('.listing-preview').first()).toContainText('Generierter Inserat-Text')
|
||||
})
|
||||
|
||||
test('KI-Inserat: 503 zeigt deutschen Hinweis (saleAdConfigured=false)', async ({ page, mockDb }) => {
|
||||
skipUnlessMock()
|
||||
mockDb!.saleAdConfigured = false
|
||||
await gotoSection(page, de.nav.forSale)
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: ta.ai.improve }).first().click()
|
||||
await expect(page.getByText(ta.ai.notConfigured)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Vertrag-Handoff: "Abgabe abschließen" öffnet Wizard mit vorausgewählten Tieren', async ({
|
||||
page,
|
||||
}) => {
|
||||
skipUnlessMock()
|
||||
await gotoSection(page, de.nav.forSale)
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
|
||||
// Wizard öffnen (navigiert zu /vertraege/neu?tiere=sale-balu,sale-benny)
|
||||
await page.getByRole('button', { name: ta.export.finish }).first().click()
|
||||
await expect(page).toHaveURL(/\/vertraege\/neu\?tiere=/)
|
||||
await expect(page.getByRole('heading', { name: tw.title })).toBeVisible()
|
||||
// Schritt 1: Abnehmer auswählen
|
||||
await expect(page.getByText(tw.pickContact)).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Vertrag-Wizard', () => {
|
||||
test('4-Schritte-Wizard: Abnehmer → Tiere → Preis → Vertrag erzeugen', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
// Direkt mit vorausgewählten Tieren öffnen (wie über Abgabe-Seite)
|
||||
await page.goto('/vertraege/neu?tiere=sale-balu,sale-benny')
|
||||
await expect(page.getByRole('heading', { name: tw.title })).toBeVisible()
|
||||
|
||||
// Schritt 1: Abnehmer wählen — Zoohandlung Meier aus dem Seed
|
||||
await expect(page.getByText(tw.pickContact)).toBeVisible()
|
||||
await page.locator('.wizard-pick').filter({ hasText: 'Zoohandlung Meier' }).getByRole('radio').check()
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
|
||||
// Schritt 2: Tiere — Vorauswahl aus ?tiere= ist aktiv
|
||||
await expect(page.getByText(tw.pickAnimals)).toBeVisible()
|
||||
// Beide ForSale-Tiere sind vorausgewählt (Checkboxen angehakt)
|
||||
await expect(
|
||||
page.locator('.wizard-pick').filter({ hasText: 'Balu Abgabe' }).getByRole('checkbox')
|
||||
).toBeChecked()
|
||||
await expect(
|
||||
page.locator('.wizard-pick').filter({ hasText: 'Benny Abgabe' }).getByRole('checkbox')
|
||||
).toBeChecked()
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
|
||||
// Schritt 3: Preis & Datum
|
||||
await page.getByLabel(tw.priceLabel).fill('50,00')
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
|
||||
// Schritt 4: Zusammenfassung + Erzeugen
|
||||
await expect(page.getByText(tw.summaryTitle)).toBeVisible()
|
||||
await expect(page.getByText('Zoohandlung Meier')).toBeVisible()
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
await page.getByRole('button', { name: tw.generate }).click()
|
||||
|
||||
// Erfolg: Vertrag erstellt, Download-Link vorhanden
|
||||
await expect(page.getByRole('heading', { name: tw.successTitle })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: tw.downloadDocx })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Schritt 1: fehlender Abnehmer zeigt Validierungsmeldung', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/vertraege/neu?tiere=sale-balu')
|
||||
await expect(page.getByText(tw.pickContact)).toBeVisible()
|
||||
// Ohne Auswahl direkt auf Weiter klicken
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
await expect(page.getByText(tw.pickContact, { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Schritt 3: ungültiger Preis zeigt Validierungsmeldung', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/vertraege/neu?tiere=sale-balu')
|
||||
// Schritt 1: Abnehmer wählen
|
||||
await page.locator('.wizard-pick').filter({ hasText: 'Zoohandlung Meier' }).getByRole('radio').check()
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
// Schritt 2: Tiere bestätigen
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
// Schritt 3: leeres Preisfeld -> Fehler
|
||||
await page.getByRole('button', { name: tw.next }).click()
|
||||
await expect(page.getByText(tw.priceInvalid)).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -101,6 +101,7 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
'color-varieties': collection(db.colorVarieties as unknown as Row[], 'cv'),
|
||||
'health-records': collection(db.healthRecords as unknown as Row[], 'hr'),
|
||||
'weight-records': collection(db.weightRecords as unknown as Row[], 'wr'),
|
||||
contracts: collection(db.contracts as unknown as Row[], 'contract'),
|
||||
}
|
||||
|
||||
const handler = async (route: Route) => {
|
||||
@@ -205,6 +206,25 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
return json(route, 405)
|
||||
}
|
||||
|
||||
// ABGABE: KI-Inserat-Generator (POST /gerbils/sale-ad) — vor der generischen Route
|
||||
if (path === '/gerbils/sale-ad' && method === 'POST') {
|
||||
if (!db.saleAdConfigured) {
|
||||
return json(route, 503, { code: 'AiKeyMissing', message: 'AI nicht konfiguriert' })
|
||||
}
|
||||
return json(route, 200, { text: 'Status: FREI\n\n„Zwei Freunde suchen ein Zuhause" – Generierter Inserat-Text für den Mock.' })
|
||||
}
|
||||
|
||||
// ABGABE: Vertrag-Download — /contracts/{id}/file (3 Segmente, nicht vom Generic-Handler bedient)
|
||||
const cm = path.match(/^\/contracts\/([^/]+)\/file$/)
|
||||
if (cm && method === 'GET') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
headers: { 'Content-Disposition': `attachment; filename="vertrag-${cm[1]}.docx"` },
|
||||
body: Buffer.from([0x50, 0x4b, 0x05, 0x06, ...new Array(18).fill(0)]),
|
||||
})
|
||||
}
|
||||
|
||||
// SEARCH-2b: distinct Herkunft (originBreeder) values, sorted — vor der
|
||||
// generischen /gerbils/:id-Route abfangen.
|
||||
if (path === '/gerbils/breeders' && method === 'GET') {
|
||||
@@ -313,6 +333,34 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
}
|
||||
}
|
||||
|
||||
// ABGABE: POST /contracts — baut url-Feld und markiert Tiere als Abgegeben
|
||||
if (path === '/contracts' && method === 'POST') {
|
||||
const body = request.postDataJSON() as { contactId: string; gerbilIds: string[]; price: number; handoverDate: string; contractDate?: string | null }
|
||||
const id = newId('contract')
|
||||
const contract = {
|
||||
id,
|
||||
contactId: body.contactId,
|
||||
gerbilIds: body.gerbilIds ?? [],
|
||||
price: body.price,
|
||||
handoverDate: body.handoverDate,
|
||||
contractDate: body.contractDate ?? body.handoverDate,
|
||||
fileName: `vertrag-${id}.docx`,
|
||||
createdAt: new Date().toISOString(),
|
||||
url: `/contracts/${id}/file`,
|
||||
}
|
||||
;(db.contracts as unknown as Row[]).push(contract as unknown as Row)
|
||||
// Tiere auf GivenAway setzen (wie das echte Backend)
|
||||
for (const gid of body.gerbilIds ?? []) {
|
||||
const g = db.gerbils.find((x) => x.id === gid)
|
||||
if (g) {
|
||||
g.status = 'GivenAway'
|
||||
g.receiverContactId = body.contactId
|
||||
g.goHomeDate = body.handoverDate
|
||||
}
|
||||
}
|
||||
return json(route, 201, contract)
|
||||
}
|
||||
|
||||
// Generische Kollektionen: /<resource> und /<resource>/<id>
|
||||
m = path.match(/^\/([a-z-]+)(?:\/([^/]+))?$/)
|
||||
const col = m ? collections[m[1]] : undefined
|
||||
|
||||
@@ -44,6 +44,19 @@ export interface MockPage {
|
||||
blocks: MockBlock[]
|
||||
}
|
||||
|
||||
/** ABGABE: Abgabevertrag (mirrors SaleContract in src/api/contracts.ts). */
|
||||
export interface MockContract {
|
||||
id: string
|
||||
contactId: string
|
||||
price: number
|
||||
handoverDate: string
|
||||
contractDate: string
|
||||
fileName: string
|
||||
createdAt: string
|
||||
gerbilIds: string[]
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface MockDb {
|
||||
gerbils: Gerbil[]
|
||||
litters: Litter[]
|
||||
@@ -58,6 +71,9 @@ export interface MockDb {
|
||||
requests: InboxRequest[]
|
||||
aiConfigured: boolean
|
||||
mailConfigured: boolean
|
||||
// ABGABE: Verträge + KI-Inserat-Flag
|
||||
contracts: MockContract[]
|
||||
saleAdConfigured: boolean
|
||||
}
|
||||
|
||||
function gerbil(
|
||||
@@ -119,6 +135,19 @@ export function seedDb(): MockDb {
|
||||
{ ...gerbil('willi', 'Willi', 'male', '2019-09-09', null, 'cv-schwarz'), status: 'Deceased', dateOfDeath: '2022-04-04' },
|
||||
{ ...gerbil('rosa', 'Rosa', 'female', '2020-02-02', null, 'cv-gold'), status: 'Deceased', dateOfDeath: '2023-08-15' },
|
||||
{ ...gerbil('pauli', 'Pauli', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-huber' },
|
||||
// ABGABE: zwei Tiere zur Abgabe im Großbecken (Gruppe 1 für den Abgabe-Wizard-Test)
|
||||
{
|
||||
...gerbil('sale-balu', 'Balu Abgabe', 'male', '2024-03-12', null, 'cv-agouti'),
|
||||
status: 'ForSale' as const,
|
||||
enclosureId: 'enc-gross',
|
||||
notes: 'ruhig, nimmt Leckerlis aus der Hand',
|
||||
},
|
||||
{
|
||||
...gerbil('sale-benny', 'Benny Abgabe', 'male', '2024-03-12', null, 'cv-schwarz-schecke'),
|
||||
status: 'ForSale' as const,
|
||||
enclosureId: 'enc-gross',
|
||||
notes: 'neugieriger Entdecker',
|
||||
},
|
||||
]
|
||||
|
||||
const litters: Litter[] = [
|
||||
@@ -258,5 +287,7 @@ export function seedDb(): MockDb {
|
||||
requests,
|
||||
aiConfigured: true,
|
||||
mailConfigured: true,
|
||||
contracts: [],
|
||||
saleAdConfigured: true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user