From 13da8f250269663e75e5e3d6ed52b25c75353624 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:08:56 +0200 Subject: [PATCH 1/6] =?UTF-8?q?QA-2:=20POLISH-2=20=E2=80=94=20verbatim=20f?= =?UTF-8?q?ew-shots=20von=20kleine-chaoten.jimdofree.com=20(Gruppe=201+4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GerbilManager.Tests/SaleAdTests.cs | 4 +- .../SaleAd/SaleAdPromptBuilder.cs | 44 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/GerbilManager.Tests/SaleAdTests.cs b/GerbilManager.Tests/SaleAdTests.cs index 2134915..11c99bd 100644 --- a/GerbilManager.Tests/SaleAdTests.cs +++ b/GerbilManager.Tests/SaleAdTests.cs @@ -67,9 +67,9 @@ namespace GerbilManager.Tests Assert.Contains("Zucht der kleinen Chaoten", prompt); Assert.Contains("NIEMALS Preise", prompt); Assert.Contains("KEINE Fakten erfinden", prompt); - // few-shot: die beiden Beispiel-Inserate (Status-Zeilen der Vorlage) + // few-shot: verbatim listings from kleine-chaoten.jimdofree.com + Assert.Contains("Status: LOCKER RESERVIERT LEONIE", prompt); Assert.Contains("Status: FREI", prompt); - Assert.Contains("Status: LOCKER RESERVIERT Anna", prompt); Assert.Contains("Großer und kleiner Bruder Dynamik", prompt); } diff --git a/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs b/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs index 73f75b9..2e45757 100644 --- a/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs +++ b/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs @@ -13,38 +13,32 @@ namespace GerbilManagerWebAPI.SaleAd public static class SaleAdPromptBuilder { /// - /// Two few-shot examples derived from the documented listing template. - /// NOTE: the research file documents the format and one real tagline; - /// these examples are synthesized to that template. Swap in verbatim - /// listings from kleine-chaoten.jimdofree.com when available. + /// Two verbatim listings from kleine-chaoten.jimdofree.com (retrieved 2026-06-06). + /// These are real listings in the breeder's own voice — the strongest grounding source. /// - private const string ExampleListing1 = """ + private const string ExampleListing1 = “”” + Status: LOCKER RESERVIERT LEONIE + + „Großer und kleiner Bruder Dynamik” – Männliches Rennmaus Duo sucht ein liebevolles Zuhause + + Hier steht ein außergewöhnlich harmonisches Rennmaus-Duo zur Abgabe frei, das nicht nur durch seinen Charakter, sondern auch durch seine besondere Geschichte überzeugt. Die zwei verhalten sich als ob sie Brüder wären und sind unzertrennlich. Das Duo kann ab sofort ausziehen. + + Der Ältere von beiden trägt die Farbe CP-Agouti und ist am 11.11.2025 geboren. Er ist ein neugieriger, aktiver und sozialer kleiner Kerl. Er liebt es, seine Umgebung zu erkunden, ist gerne in Bewegung und bringt dabei eine angenehme Ausgeglichenheit mit. Er wirkt sicher, aufmerksam und zeigt ein rundum unkompliziertes Wesen. + + Sein, etwas jünger, Partner trägt die Farbe Schwarz Schecke und ist am 26.02.2026 geboren. Statt des in diesem Alter oft typischen Kräftemessens zeigt er ein erstaunlich sanftes, ruhiges und sehr soziales Verhalten. Man merkt ihm an, wie feinfühlig und verträglich er ist. + “””; + + private const string ExampleListing2 = “”” Status: FREI - „Großer und kleiner Bruder Dynamik“ – Männliches Rennmaus-Duo sucht ein liebevolles Zuhause + „Unzertrennliches Mutter-Tochter Duo sucht gemeinsam neue Körnergeber” - Balu – CP-Agouti, geboren am 12.03.2024 - Balu ist der ruhige Pol des Duos: Er beobachtet erst in aller Seelenruhe und buddelt sich dann zielstrebig durch jedes Einstreu-Projekt. Aus der Hand nimmt er Leckerlis schon ganz vorsichtig. + Die Mama ist am 13.08.2025 geboren und trägt die Farbe Kohlfuchs. Sie ist eine eher ruhige und vorsichtige Rennmaus, die neuen Situationen zunächst aufmerksam begegnet. Gibt man ihr etwas Zeit, zeigt sich schnell ihre neugierige Seite und sie kommt gerne schauen, was im Gehege passiert. Sie ist ein echtes Energiebündel, liebt es zu buddeln, zu rennen und alles an Beschäftigungsmaterial – besonders Klorollen – kreativ umzugestalten. Charakterlich zeigt sie sich sehr sozial und friedlich; besonders hervorzuheben ist ihre fürsorgliche Art als Mutter. - Benny – Schwarz Schecke, geboren am 12.03.2024 - Benny ist der Entdecker: kein Röhrchen bleibt unerforscht, kein Häuschen unbewohnt. Mit seinem Bruder kuschelt er sich abends ins Nest — getrennt werden die beiden deshalb nicht. + Die Tochter ist am 26.02.2026 geboren und trägt die Farbe Agouti-Schecke. Sie ist ihrer Mama sehr ähnlich, dabei aber schon ein kleines bisschen mutiger. Auch sie beobachtet erst gerne in Ruhe, bevor die Neugier siegt. Sie ist sehr anhänglich und orientiert sich stark an ihrer Mutter. Die zwei werden nur gemeinsam in ein rennmausgerechtes Zuhause abgegeben. - """; - - private const string ExampleListing2 = """ - Status: LOCKER RESERVIERT Anna - - „Zwei Schwestern, ein Herz und ganz viel Neugier“ – Weibliches Duo sucht seine Menschen - - Frieda – Gold, geboren am 28.06.2024 - Frieda ist die Mutige der beiden und steht beim Öffnen des Geheges sofort am Glas. Sie liebt Kolbenhirse und nimmt sie dir behutsam aus den Fingern. - - Fine – Agouti, geboren am 28.06.2024 - Fine ist etwas zurückhaltender, taut aber neben ihrer Schwester schnell auf. Beim abendlichen Buddeln sind die zwei ein unschlagbares Team. - - Die Schwestern ziehen selbstverständlich nur zusammen um. - """; + “””; /// System prompt: role, style description, hard rules, few-shot examples. public static string BuildSystemPrompt() => $""" From ddfa3614a4310564cf10c5a763c1f0496636d118 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:09:15 +0200 Subject: [PATCH 2/6] =?UTF-8?q?QA-2:=20CI-Flake=20fix=20=E2=80=94=20expliz?= =?UTF-8?q?ite=20Heading-Waits=20nach=20gotoSection=20in=20'Kontakt=20anle?= =?UTF-8?q?gen'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gerbil-manager-web/e2e/becken-kontakte.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gerbil-manager-web/e2e/becken-kontakte.spec.ts b/gerbil-manager-web/e2e/becken-kontakte.spec.ts index be82dcf..7a8feb8 100644 --- a/gerbil-manager-web/e2e/becken-kontakte.spec.ts +++ b/gerbil-manager-web/e2e/becken-kontakte.spec.ts @@ -72,7 +72,13 @@ test.describe('Kontakte', () => { test('Kontakt anlegen und (unverknüpft) wieder löschen', async ({ page }) => { await gotoSection(page, de.nav.contacts) + // Explizit auf die Listenüberschrift warten, bevor wir klicken — verhindert den + // CI-Flake, der auftrat wenn der Vite-Server unter paralleler Last noch nicht + // alle Routen der Seite gerendert hatte (gotoSection klickt nur den Nav-Link, + // wartet aber nicht auf den vollständigen Seitenaufbau). + await expect(page.getByRole('heading', { name: tk.title, exact: true })).toBeVisible() await page.getByRole('link', { name: tk.newButton }).click() + await expect(page.getByRole('heading', { name: tk.form.createTitle })).toBeVisible() const name = uniqueName('Kontakt') await page.getByLabel(`${tk.fields.name} *`).fill(name) // FEAT-13: strukturierte Felder statt Freitext-Kontaktdaten From 868c5f5cd7e530d4ab8ab7b853196968ff436cc5 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:11:22 +0200 Subject: [PATCH 3/6] QA-2: skipUnlessLive() + Live-Smoke (Liste>=10, Suche Treffer, Eltern-Links) --- gerbil-manager-web/e2e/fixtures.ts | 11 ++++++ gerbil-manager-web/e2e/live.spec.ts | 57 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 gerbil-manager-web/e2e/live.spec.ts diff --git a/gerbil-manager-web/e2e/fixtures.ts b/gerbil-manager-web/e2e/fixtures.ts index 358f507..60ea644 100644 --- a/gerbil-manager-web/e2e/fixtures.ts +++ b/gerbil-manager-web/e2e/fixtures.ts @@ -39,6 +39,17 @@ export function skipUnlessMock() { test.skip(LIVE, 'benötigt die Mock-Seed-Daten (läuft nicht im LIVE-Modus)') } +/** + * Test überspringen im Mock-Modus — nur im LIVE-Modus sinnvoll. + * Live-Tests MÜSSEN Ergebnisse assertieren (z. B. Liste nicht leer, + * konkreter Wert sichtbar), nicht nur HTTP-200. Lesson: der Mock spiegelt + * den Frontend-Code, nicht das echte Backend — ein Bug bleibt unsichtbar, + * wenn wir nur testen, ob die Seite lädt. + */ +export function skipUnlessLive() { + test.skip(!LIVE, 'nur im LIVE-Modus sinnvoll (E2E_BASE_URL setzen)') +} + /** * Zu einem Navigationsziel wechseln — auf dem Smartphone liegen Becken, * Kontakte und Statistik hinter dem „Mehr“-Blatt. diff --git a/gerbil-manager-web/e2e/live.spec.ts b/gerbil-manager-web/e2e/live.spec.ts new file mode 100644 index 0000000..ce495eb --- /dev/null +++ b/gerbil-manager-web/e2e/live.spec.ts @@ -0,0 +1,57 @@ +/** + * QA-2: Live-Modus-Smoke — läuft NUR gegen das echte Backend (E2E_BASE_URL). + * + * Lektion (aus dem Such-Bug): Mock-Tests spiegeln nur den Frontend-Code. + * Ein Backend-Bug (falscher Filter, leere Antwort, fehlende Migration) bleibt + * unsichtbar, wenn Live-Tests nur "Seite lädt" prüfen. Diese Tests assertieren + * ERGEBNISSE: Liste nicht leer, Suche liefert Treffer, Detail zeigt Stammdaten + * und Eltern-Links. + * + * Voraussetzung für den Eltern-Link-Test: Re-Import #2 durchgeführt + * (322 Tiere, 882 Würfe live; mindestens ein Wurf hat beide Eltern). + */ +import { de, expect, gotoSection, skipUnlessLive, test } from './fixtures' + +const t = de.pages.gerbils +const tl = de.pages.litters + +test('Live: Tiere-Liste enthält nach Re-Import mindestens 10 Einträge', async ({ page }) => { + skipUnlessLive() + await gotoSection(page, de.nav.gerbils) + await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible() + // Leere Liste = Backend-Bug (falscher Filter, fehlende DB-Migration o. ä.) + const rows = page.locator('.gerbil-row') + await expect(rows.first()).toBeVisible() + expect(await rows.count()).toBeGreaterThanOrEqual(10) +}) + +test('Live: Namenssuche gibt Treffer zurück — nicht nur HTTP 200', async ({ page }) => { + skipUnlessLive() + await gotoSection(page, de.nav.gerbils) + await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible() + // Kurzer Prefix: bei 322 Tieren mindestens 1 Treffer. Kein Treffer = Gridify-Bug. + await page.getByPlaceholder(t.searchPlaceholder).first().fill('a') + const rows = page.locator('.gerbil-row') + await expect(rows.first()).toBeVisible({ timeout: 10_000 }) + expect(await rows.count()).toBeGreaterThanOrEqual(1) +}) + +test('Live: Wurf-Detail zeigt Elterntier-Links (mindestens Vater oder Mutter)', async ({ + page, +}) => { + skipUnlessLive() + // Nach Re-Import #2 hat mindestens Gerbil C (Victoria) beide Eltern. + // Der Wurf-Detail ist die einfachste Ansicht, die Eltern-Links rendert. + await gotoSection(page, de.nav.litters) + await expect(page.getByRole('heading', { name: tl.title, exact: true })).toBeVisible() + + // Mindestens 1 Wurf in der Liste; ersten öffnen. + const firstLink = page.getByRole('link').filter({ hasText: /.+/ }).nth(1) + await expect(firstLink).toBeVisible() + await firstLink.click() + + // Detailseite: Elterntier-Abschnitt ist sichtbar + await expect(page.getByText(tl.detail.parents)).toBeVisible() + // Mindestens ein Link führt zu einem Tier — bei fehlendem Import fehlt dieser Link + await expect(page.locator('a[href*="/rennmaeuse/"]').first()).toBeVisible({ timeout: 5_000 }) +}) From 791e9f91cc2534b16bd2bf70297f67d77ada937a Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:16:38 +0200 Subject: [PATCH 4/6] QA-2: Abgabe-Wizard e2e (ForSale-Seed, sale-ad+contracts Mock, abgabe.spec.ts) --- gerbil-manager-web/e2e/abgabe.spec.ts | 127 ++++++++++++++++++++++++++ gerbil-manager-web/e2e/mock-api.ts | 48 ++++++++++ gerbil-manager-web/e2e/mock-data.ts | 31 +++++++ 3 files changed, 206 insertions(+) create mode 100644 gerbil-manager-web/e2e/abgabe.spec.ts diff --git a/gerbil-manager-web/e2e/abgabe.spec.ts b/gerbil-manager-web/e2e/abgabe.spec.ts new file mode 100644 index 0000000..8834292 --- /dev/null +++ b/gerbil-manager-web/e2e/abgabe.spec.ts @@ -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() + }) +}) diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index dbceadf..1e74a7a 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -101,6 +101,7 @@ export async function installMockApi(page: Page): Promise { '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 { 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 { } } + // 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: / und // m = path.match(/^\/([a-z-]+)(?:\/([^/]+))?$/) const col = m ? collections[m[1]] : undefined diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index f481ba1..a3bd834 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -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, } } From c357a184183d9287f0a32c3b904868fb6685ca46 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:22:34 +0200 Subject: [PATCH 5/6] QA-2: Fix Abgabe-e2e Selektoren + ForSale in enc-leer (no Becken-Konflikt) + charakter scoped --- gerbil-manager-web/e2e/abgabe.spec.ts | 40 +++++++++++++----------- gerbil-manager-web/e2e/charakter.spec.ts | 7 ++++- gerbil-manager-web/e2e/mock-data.ts | 7 +++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/gerbil-manager-web/e2e/abgabe.spec.ts b/gerbil-manager-web/e2e/abgabe.spec.ts index 8834292..b62487c 100644 --- a/gerbil-manager-web/e2e/abgabe.spec.ts +++ b/gerbil-manager-web/e2e/abgabe.spec.ts @@ -2,37 +2,36 @@ * 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. + * Mock-gebunden (Seed: 2 ForSale-Tiere im Quarantänebecken) → skipUnlessMock. */ -import { acceptNextDialog, de, expect, gotoSection, skipUnlessMock, test } from './fixtures' +import { 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 }) => { + test('Abgabe-Seite zeigt ForSale-Tiere und Inserat-Vorschau', 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() + // Die beiden ForSale-Tiere müssen als Tiername (strong-Element im Komposer) sichtbar sein + await expect(page.locator('.group-composer__animal-head strong').filter({ hasText: 'Balu Abgabe' })).toBeVisible() + await expect(page.locator('.group-composer__animal-head strong').filter({ hasText: '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() + page.locator('label.field').filter({ has: page.locator(`span:text-is("${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 expect(page.locator('.group-composer__animal-head strong').filter({ hasText: 'Balu Abgabe' })).toBeVisible() await page.getByRole('button', { name: ta.ai.improve }).first().click() // Mock antwortet synchron; der generierte Text erscheint in der Vorschau @@ -43,7 +42,7 @@ test.describe('Abgabe', () => { skipUnlessMock() mockDb!.saleAdConfigured = false await gotoSection(page, de.nav.forSale) - await expect(page.getByText('Balu Abgabe')).toBeVisible() + await expect(page.locator('.group-composer__animal-head strong').filter({ hasText: 'Balu Abgabe' })).toBeVisible() await page.getByRole('button', { name: ta.ai.improve }).first().click() await expect(page.getByText(ta.ai.notConfigured)).toBeVisible() @@ -54,14 +53,14 @@ test.describe('Abgabe', () => { }) => { skipUnlessMock() await gotoSection(page, de.nav.forSale) - await expect(page.getByText('Balu Abgabe')).toBeVisible() + await expect(page.locator('.group-composer__animal-head strong').filter({ hasText: '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() + await expect(page.getByRole('heading', { name: tw.pickContact })).toBeVisible() }) }) @@ -73,12 +72,12 @@ test.describe('Vertrag-Wizard', () => { 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 expect(page.getByRole('heading', { name: 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() + await expect(page.getByRole('heading', { name: tw.pickAnimals })).toBeVisible() // Beide ForSale-Tiere sind vorausgewählt (Checkboxen angehakt) await expect( page.locator('.wizard-pick').filter({ hasText: 'Balu Abgabe' }).getByRole('checkbox') @@ -103,13 +102,14 @@ test.describe('Vertrag-Wizard', () => { await expect(page.getByRole('link', { name: tw.downloadDocx })).toBeVisible() }) - test('Schritt 1: fehlender Abnehmer zeigt Validierungsmeldung', async ({ page }) => { + test('Schritt 1: fehlender Abnehmer zeigt Validierungsfehler', 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 expect(page.getByRole('heading', { name: tw.pickContact })).toBeVisible() + // Ohne Auswahl direkt auf Weiter klicken → Fehler-Alert erscheint await page.getByRole('button', { name: tw.next }).click() - await expect(page.getByText(tw.pickContact, { exact: true })).toBeVisible() + // Alert enthält den Hinweis (stepError), Schritt bleibt auf 1 + await expect(page.locator('.alert--error').filter({ hasText: tw.pickContact })).toBeVisible() }) test('Schritt 3: ungültiger Preis zeigt Validierungsmeldung', async ({ page }) => { @@ -119,9 +119,11 @@ test.describe('Vertrag-Wizard', () => { 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 expect(page.getByRole('heading', { name: tw.pickAnimals })).toBeVisible() await page.getByRole('button', { name: tw.next }).click() // Schritt 3: leeres Preisfeld -> Fehler + await expect(page.getByLabel(tw.priceLabel)).toBeVisible() await page.getByRole('button', { name: tw.next }).click() - await expect(page.getByText(tw.priceInvalid)).toBeVisible() + await expect(page.locator('.alert--error').filter({ hasText: tw.priceInvalid })).toBeVisible() }) }) diff --git a/gerbil-manager-web/e2e/charakter.spec.ts b/gerbil-manager-web/e2e/charakter.spec.ts index 1cfdc50..65bcfc1 100644 --- a/gerbil-manager-web/e2e/charakter.spec.ts +++ b/gerbil-manager-web/e2e/charakter.spec.ts @@ -17,5 +17,10 @@ test('Charakterbogen: Eigenschaft umschalten, speichern und im Abgabe-Komposer w // Tier zur Abgabe stellen -> erscheint im /abgabe-Komposer mit gesetzter Eigenschaft. await page.getByRole('button', { name: de.pages.abgabe.markAction }).click() await page.goto('/abgabe') - await expect(page.getByRole('checkbox', { name: 'neugierig' })).toBeChecked() + // Auf Krümels Tier-Abschnitt eingrenzen — andere ForSale-Tiere haben ebenfalls + // Charakterbögen, deshalb darf die Assertion nicht seitenbreit nach 'neugierig' suchen. + const kruemelSection = page.locator('.group-composer__animal').filter({ + has: page.locator('strong', { hasText: 'Krümel' }), + }) + await expect(kruemelSection.getByRole('checkbox', { name: 'neugierig' })).toBeChecked() }) diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index a3bd834..efdfec4 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -135,17 +135,18 @@ 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) + // ABGABE: zwei Tiere zur Abgabe im Quarantänebecken (enc-leer ist leer, kein Konflikt + // mit dem Becken-Belegungstest der Großbecken-2-Tiere-Assertion). { ...gerbil('sale-balu', 'Balu Abgabe', 'male', '2024-03-12', null, 'cv-agouti'), status: 'ForSale' as const, - enclosureId: 'enc-gross', + enclosureId: 'enc-leer', 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', + enclosureId: 'enc-leer', notes: 'neugieriger Entdecker', }, ] From 68447896ee67083826f5a04f5729c032e7c9c477 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 15:30:46 +0200 Subject: [PATCH 6/6] QA-2: Fix smart-quote raw-string delimiters in SaleAdPromptBuilder (POLISH-2 BOUNCE) --- GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs b/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs index 2e45757..2c949f4 100644 --- a/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs +++ b/GerbilManagerWebAPI/SaleAd/SaleAdPromptBuilder.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; namespace GerbilManagerWebAPI.SaleAd @@ -16,7 +16,7 @@ namespace GerbilManagerWebAPI.SaleAd /// Two verbatim listings from kleine-chaoten.jimdofree.com (retrieved 2026-06-06). /// These are real listings in the breeder's own voice — the strongest grounding source. /// - private const string ExampleListing1 = “”” + private const string ExampleListing1 = """ Status: LOCKER RESERVIERT LEONIE „Großer und kleiner Bruder Dynamik” – Männliches Rennmaus Duo sucht ein liebevolles Zuhause @@ -26,9 +26,9 @@ namespace GerbilManagerWebAPI.SaleAd Der Ältere von beiden trägt die Farbe CP-Agouti und ist am 11.11.2025 geboren. Er ist ein neugieriger, aktiver und sozialer kleiner Kerl. Er liebt es, seine Umgebung zu erkunden, ist gerne in Bewegung und bringt dabei eine angenehme Ausgeglichenheit mit. Er wirkt sicher, aufmerksam und zeigt ein rundum unkompliziertes Wesen. Sein, etwas jünger, Partner trägt die Farbe Schwarz Schecke und ist am 26.02.2026 geboren. Statt des in diesem Alter oft typischen Kräftemessens zeigt er ein erstaunlich sanftes, ruhiges und sehr soziales Verhalten. Man merkt ihm an, wie feinfühlig und verträglich er ist. - “””; + """; - private const string ExampleListing2 = “”” + private const string ExampleListing2 = """ Status: FREI „Unzertrennliches Mutter-Tochter Duo sucht gemeinsam neue Körnergeber” @@ -38,7 +38,7 @@ namespace GerbilManagerWebAPI.SaleAd Die Tochter ist am 26.02.2026 geboren und trägt die Farbe Agouti-Schecke. Sie ist ihrer Mama sehr ähnlich, dabei aber schon ein kleines bisschen mutiger. Auch sie beobachtet erst gerne in Ruhe, bevor die Neugier siegt. Sie ist sehr anhänglich und orientiert sich stark an ihrer Mutter. Die zwei werden nur gemeinsam in ein rennmausgerechtes Zuhause abgegeben. - “””; + """; /// System prompt: role, style description, hard rules, few-shot examples. public static string BuildSystemPrompt() => $"""