7335f64f4f
CI / loc-budget (push) Failing after 20s
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 19s
CI / nodejs-build (push) Successful in 3m17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Wizard unterstuetzt jetzt 2-4 Gesellschafter mit individuellem IP-Bereich: - Pro Gruender ein IP-Assignment-Vertrag (z.B. Benjamin: Compliance+RAG; Sharang: Security+Infrastruktur). Pro GF ein eigener Dienstvertrag. - Step 1: Prefill-Button aus Unternehmensprofil + Felder Registergericht und HRB-Nr. - Step 2: Rollen-Dropdown (CEO/CTO/CFO/COO/CPO/GF/Sonstige) statt freie Texteingabe, IP-Bereiche-Textarea pro Person. Backend: - generate_documents() iteriert pro Person fuer PER_PERSON_DOCS. - _build_person_context() injiziert ASSIGNOR_*, GF_*, IP_LIST_DETAILS aus person.ip_areas. - base_context() propagiert basics.register_court und basics.hrb_number. Tests: - 30/30 Pytest gruen (6 neue: Per-Person-Context, Slug-Helper, Registergericht-Propagation). - 4 neue Playwright-E2E-Specs (hermetisch via route.fulfill, mit Console-/Page-Error-Traps): kompletter 8-Step-Flow, Prefill-Fehlerpfad, Step-Navigation/Reset, Rollen-Dropdown + IP-Areas. - Spec setzt 'bp-sdk-cookie-consent' im addInitScript damit der CookieBannerOverlay nicht die Wizard-Buttons ueberlagert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
356 lines
15 KiB
TypeScript
356 lines
15 KiB
TypeScript
/**
|
|
* 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 <select> sein, nicht <input>
|
|
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()
|
|
})
|
|
})
|