/** * E2E-Test fuer den Founding-Wizard * * Prueft den vollstaendigen 8-Step-Flow: * - Application-Errors / Console-Errors auf jeder Seite * - StepBasics: Prefill-Button + Registergericht/HRB-Felder * - StepGesellschafter: Rollen-Dropdown + IP-Bereiche fuer 2 Gruender * - Per-Person Generation: 2 IP-Assignment-Dokumente * - localStorage-Persistenz * * Backend wird per route.fulfill() gemockt — Test ist hermetisch. */ import { test, expect, type Page, type ConsoleMessage } from '@playwright/test' const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3002' const WIZARD_PATH = '/sdk/founding-wizard' /** Filtert Browser-Console auf echte App-Errors (ignoriert Next.js / Hydration / 3rd-party Warnings). */ function isRealAppError(msg: ConsoleMessage): boolean { if (msg.type() !== 'error') return false const text = msg.text() // Bekanntes Rauschen ausschliessen const ignored = [ 'Failed to load resource', // 404 fuer Icons etc. 'Download the React DevTools', // React-Hinweis 'net::ERR_', // Netzwerk (gemockt → erwartete Misses) 'Hydration failed because', // Next 15 Pseudo-Errors bei dev '[founding-wizard] prefill failed', // Intentional UX-Logging im Prefill-Fehlerpfad ] return !ignored.some(p => text.includes(p)) } const IGNORED_PAGE_ERRORS = [ // Hydration mismatches durch dynamische Zeitstempel ("Gerade eben" vs "vor 1 Min") // im SDK-Header — pure dev-Mode-Symptom, kein App-Bug. 'Hydration failed because the server rendered text didn', 'There was an error while hydrating', // Next.js dev-mode signals fuer Hydration-Issues 'Text content does not match server-rendered HTML', ] function isIgnoredPageError(err: Error): boolean { return IGNORED_PAGE_ERRORS.some(p => err.message.includes(p)) } /** Setzt Console-Error- und PageError-Listener. Wirft am Ende, wenn welche aufgetreten sind. */ function installErrorTraps(page: Page): { assertNoErrors: () => void } { const consoleErrors: string[] = [] const pageErrors: string[] = [] page.on('console', msg => { if (isRealAppError(msg)) consoleErrors.push(msg.text()) }) page.on('pageerror', err => { if (!isIgnoredPageError(err)) pageErrors.push(`${err.name}: ${err.message}`) }) return { assertNoErrors() { const all = [...pageErrors.map(e => `[pageerror] ${e}`), ...consoleErrors.map(e => `[console.error] ${e}`)] if (all.length > 0) { throw new Error(`Application-Errors waehrend des Flows:\n${all.join('\n')}`) } }, } } /** Mockt die zwei API-Endpoints, die der Wizard aufruft. */ async function mockBackend(page: Page) { // 1) Company-Profile Prefill await page.route('**/api/sdk/v1/company-profile**', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ profile: { companyName: 'Breakpilot GmbH', legalForm: 'GmbH', industry: ['Software', 'KI/ML'], businessModel: 'SaaS', offerings: ['SaaS-Plattform', 'Compliance-API'], headquartersStreet: 'Königstraße 1', headquartersZip: '70173', headquartersCity: 'Stuttgart', }, }), }) }) // 2) Founding-Wizard Generate (gibt 9 Dokumente zurueck: 7 normale + 2 per-person IP-Assignments) await page.route('**/api/v1/founding-wizard/generate', async route => { const request = route.request() const body = JSON.parse(request.postData() || '{}') const selected: string[] = body.selected_documents || [] const gesellschafter: Array<{ name?: string; is_geschaeftsfuehrer?: boolean }> = body.gesellschafter || [] const PER_PERSON = ['ip_assignment_agreement', 'managing_director_employment_contract'] const docs: unknown[] = [] const tinyDocx = 'UEsDBBQAAAAIAA==' // gueltige base64-Stub (Playwright braucht keinen echten DOCX) for (const docType of selected) { if (PER_PERSON.includes(docType)) { const persons = docType === 'managing_director_employment_contract' ? gesellschafter.filter(g => g.is_geschaeftsfuehrer) : gesellschafter for (const p of persons) { docs.push({ document_type: docType, title: `${docType} — ${p.name}`, filename: `${docType}_${(p.name || 'X').replace(/\s/g, '_')}.docx`, download_url: `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${tinyDocx}`, size_bytes: 12345, generated_at: '2026-05-21T12:00:00Z', }) } } else { docs.push({ document_type: docType, title: docType, filename: `${docType}.docx`, download_url: `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${tinyDocx}`, size_bytes: 12345, generated_at: '2026-05-21T12:00:00Z', }) } } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ documents: docs, warnings: [] }), }) }) } /** Clears wizard-state and pre-accepts cookies so the CookieBannerOverlay * does not intercept clicks during the test. */ async function resetWizardState(page: Page) { await page.addInitScript(() => { try { window.localStorage.removeItem('breakpilot:founding-wizard:state:v1') // CookieBannerOverlay liest 'bp-sdk-cookie-consent' und blendet sich aus, // sobald ein Eintrag existiert. Wir setzen Minimal-Consent. window.localStorage.setItem('bp-sdk-cookie-consent', JSON.stringify({ necessary: true, statistics: false, marketing: false, functional: false, ewrOnly: false, blockedVendors: [], timestamp: new Date().toISOString(), })) } catch {} }) } test.describe('Founding-Wizard E2E', () => { test.beforeEach(async ({ page }) => { await resetWizardState(page) await mockBackend(page) }) test('vollstaendiger 8-Step-Flow ohne Application-Errors', async ({ page }) => { const errors = installErrorTraps(page) await page.goto(`${BASE}${WIZARD_PATH}`) await expect(page.getByTestId('founding-wizard')).toBeVisible() await expect(page.getByTestId('step-content-1')).toBeVisible() // --- Step 1: Basics + Prefill --- await page.getByRole('button', { name: /Aus Unternehmensprofil vorbef/i }).click() await expect(page.getByTestId('company-name')).toHaveValue('Breakpilot GmbH', { timeout: 5000 }) await expect(page.getByTestId('company-seat')).toHaveValue('Stuttgart') // Pflichtfeld: company_purpose_description (mind. 10 Zeichen) await page.getByTestId('company-purpose').fill( 'die Entwicklung, Bereitstellung und der Betrieb von KI-gestuetzten Compliance-Werkzeugen sowie damit verbundener Beratungsleistungen.' ) // Neue Felder: Registergericht + HRB await page.getByTestId('register-court').fill('Amtsgericht Stuttgart') await page.getByTestId('hrb-number').fill('') // noch nicht eingetragen await page.getByTestId('next-step').click() // --- Step 2: Gesellschafter --- await expect(page.getByTestId('step-content-2')).toBeVisible() // Benjamin (CEO, IP: Compliance + RAG) await page.getByTestId('gs-name').fill('Benjamin Bönisch') await page.getByTestId('gs-birthdate').fill('1985-01-15') await page.getByTestId('gs-address').fill('Teststraße 1, 70173 Stuttgart') await page.getByTestId('gs-email').fill('benjamin@breakpilot.ai') await page.getByTestId('gs-nennbetrag').fill('12500') await page.getByTestId('gs-role').selectOption('CEO') await page.getByTestId('gs-ip-areas').fill( 'Compliance-Engine (Quellcode + Architektur)\nRAG-Pipeline\nProdukt-Konzepte' ) await page.getByTestId('add-gesellschafter').click() await expect(page.getByTestId('gs-row-1')).toBeVisible() // Sharang (CTO, IP: Security + Infrastruktur) await page.getByTestId('gs-name').fill('Sharang Parnerkar') await page.getByTestId('gs-birthdate').fill('1990-06-20') await page.getByTestId('gs-address').fill('Teststraße 2, 70173 Stuttgart') await page.getByTestId('gs-email').fill('sharang@breakpilot.ai') await page.getByTestId('gs-nennbetrag').fill('12500') await page.getByTestId('gs-role').selectOption('CTO') await page.getByTestId('gs-ip-areas').fill('Security-Modul\nInfrastructure-as-Code') await page.getByTestId('add-gesellschafter').click() await expect(page.getByTestId('gs-row-2')).toBeVisible() // Summe Nennbetraege muss Stammkapital entsprechen (25.000) await expect(page.getByTestId('gs-total')).toContainText('25.000') await page.getByTestId('next-step').click() // --- Step 3: GF-Assignment (Defaults sind ok, beide bereits GF) --- await expect(page.getByTestId('step-content-3')).toBeVisible() await expect(page.getByTestId('gf-assignment-table')).toBeVisible() await page.getByTestId('next-step').click() // --- Step 4: Kapital (Defaults: 25000) --- await expect(page.getByTestId('step-content-4')).toBeVisible() await expect(page.getByTestId('stammkapital')).toHaveValue('25000') await page.getByTestId('next-step').click() // --- Step 5: Notar --- await expect(page.getByTestId('step-content-5')).toBeVisible() await page.getByTestId('notary-name').fill('Dr. Max Mustermann') await page.getByTestId('notary-place').fill('Stuttgart') await page.getByTestId('notary-address').fill('Königstraße 99, 70173 Stuttgart') await page.getByTestId('notarial-date').fill('2026-06-15') await page.getByTestId('next-step').click() // --- Step 6: SHA-Optionen (Defaults sind ok) --- await expect(page.getByTestId('step-content-6')).toBeVisible() await expect(page.getByTestId('has-sha')).toBeChecked() await page.getByTestId('next-step').click() // --- Step 7: GF-Vertraege (fuer jeden GF einen) --- await expect(page.getByTestId('step-content-7')).toBeVisible() // Beide GF-Contract-Karten muessen sichtbar sein const contractCards = page.locator('[data-testid^="contract-"]') await expect(contractCards).toHaveCount(2) // Salary in beiden Cards anfassen → registriert Contracts (canProceed-Bedingung). // Wir setzen einen anderen Wert als Default (84000) damit React onChange feuert. const salaryInputs = page.locator('[data-testid^="salary-"]') const salaryCount = await salaryInputs.count() for (let i = 0; i < salaryCount; i++) { await salaryInputs.nth(i).fill('90000') } // Warten bis "Weiter" enabled ist await expect(page.getByTestId('next-step')).toBeEnabled() await page.getByTestId('next-step').click() // --- Step 8: Generate --- await expect(page.getByTestId('step-content-8')).toBeVisible() await expect(page.getByTestId('generate-summary')).toContainText('Breakpilot GmbH') await expect(page.getByTestId('generate-summary')).toContainText('2', { useInnerText: true }) // Notartermin-Bundle auswaehlen await page.getByTestId('select-notary-bundle').click() // Generieren (Backend gemockt) await page.getByTestId('generate-docs').click() // Generated-Docs-Block muss erscheinen await expect(page.getByTestId('generated-docs')).toBeVisible({ timeout: 10000 }) // Per-Person Verifikation: zwei IP-Assignment-Downloads erwartet const ipDownloads = page.locator('[data-testid="download-ip_assignment_agreement"]') await expect(ipDownloads).toHaveCount(2) // Per-Person Verifikation: zwei GF-Vertraege erwartet const gfDownloads = page.locator('[data-testid="download-managing_director_employment_contract"]') await expect(gfDownloads).toHaveCount(2) // Kein generate-error sichtbar await expect(page.getByTestId('generate-error')).toBeHidden() // Final: keine Errors auf der Konsole errors.assertNoErrors() }) test('Prefill-Button setzt Fehler bei Backend-Fehler ohne Application-Error', async ({ page }) => { // Spezial-Mock: company-profile gibt 500 zurueck await page.route('**/api/sdk/v1/company-profile**', async route => { await route.fulfill({ status: 500, body: 'boom' }) }) const errors = installErrorTraps(page) await page.goto(`${BASE}${WIZARD_PATH}`) await page.getByRole('button', { name: /Aus Unternehmensprofil vorbef/i }).click() // UI muss Fehlermeldung anzeigen, NICHT crashen await expect(page.getByText('Konnte Unternehmensprofil nicht laden')).toBeVisible() errors.assertNoErrors() }) test('Step-Navigation: Zurueck und Reset funktionieren ohne Errors', async ({ page }) => { const errors = installErrorTraps(page) await page.goto(`${BASE}${WIZARD_PATH}`) // Minimum Step 1 fuellen await page.getByTestId('company-name').fill('Breakpilot GmbH') await page.getByTestId('company-seat').fill('Stuttgart') await page.getByTestId('company-purpose').fill('die Entwicklung von Compliance-Software fuer Unternehmen.') await page.getByTestId('next-step').click() await expect(page.getByTestId('step-content-2')).toBeVisible() // Zurueck await page.getByTestId('prev-step').click() await expect(page.getByTestId('step-content-1')).toBeVisible() // Eingaben muessen erhalten geblieben sein (localStorage-persistence) await expect(page.getByTestId('company-name')).toHaveValue('Breakpilot GmbH') // Reset (mit Dialog-Bestaetigung) page.once('dialog', dialog => dialog.accept()) await page.getByTestId('reset-wizard').click() await expect(page.getByTestId('company-name')).toHaveValue('') errors.assertNoErrors() }) test('IP-Areas + Rollen-Dropdown in Step 2', async ({ page }) => { const errors = installErrorTraps(page) await page.goto(`${BASE}${WIZARD_PATH}`) // Step 1 zuegig fuellen await page.getByTestId('company-name').fill('Breakpilot GmbH') await page.getByTestId('company-seat').fill('Stuttgart') await page.getByTestId('company-purpose').fill('die Entwicklung von Compliance-Software fuer Unternehmen.') await page.getByTestId('next-step').click() // Rollen-Dropdown muss ein const role = page.getByTestId('gs-role') await expect(role).toHaveJSProperty('tagName', 'SELECT') // CEO-Option waehlbar await page.getByTestId('gs-name').fill('Benjamin Bönisch') await page.getByTestId('gs-address').fill('Test 1') await page.getByTestId('gs-nennbetrag').fill('25000') await role.selectOption('CEO') await page.getByTestId('gs-ip-areas').fill('Compliance-Engine\nRAG-Pipeline') await page.getByTestId('add-gesellschafter').click() // Tabelle muss IP-Bereiche anzeigen const row = page.getByTestId('gs-row-1') await expect(row).toContainText('Benjamin Bönisch') await expect(row).toContainText('CEO') await expect(row).toContainText('Compliance-Engine') errors.assertNoErrors() }) })