Steuerkram/tests/e2e/specs/rechnung-export.spec.js
fds2610-bot 7cea573ffc Fix #e2e: E2E-Tests ergänzt, /accounts/new und /assets/new lieferten HTTP 500
Steuerkram an den e2e-runner angebunden (tests/e2e/, 44 Playwright-Tests).
Der erste Smoke-Lauf fand zwei Seiten mit HTTP 500: AccountController::create
las $_GET['from'] ungeprüft, assets/form.php nutzte $testMode und
$defaultKontoId außerhalb des Bearbeiten-Zweigs. Beides war bis zur
Fehler-Härtung (#142) nur eine stille Warning.

Test 67 räumt sein Belegverzeichnis jetzt mit ab - es blieb als root-eigenes
Verzeichnis liegen und blockierte spätere echte Uploads mit HTTP 500.

Test 77 (neu) sichert die Löschsperre von Beleg::delete() ab.
2026-08-02 11:58:33 +02:00

126 lines
5.2 KiB
JavaScript

// @ts-check
// Hotspot: Rechnung anlegen → finalisieren → PDF- und XML-Export.
//
// Der XML-Download (EN 16931 / XRechnung 3.0) kam zuletzt zweimal in Folge als
// Fix ins Repo — einmal der Export selbst, danach die USt-IdNr. des Kunden
// (BT-48), die im XML fehlte. Beide Male hing der Fehler am Zusammenspiel aus
// Controller, ZugferdService und den tatsächlich gespeicherten Rechnungsdaten.
//
// Hinweis: Der Finalisierungs-Test verbraucht eine Rechnungsnummer aus dem
// laufenden Nummernkreis. Das lässt sich nicht zurücknehmen (GoBD) — die
// Rechnung selbst wird im Testmodus wieder gelöscht. Siehe README.md.
const { test, expect } = require('@playwright/test')
const { uniqueTag, gotoOk, acceptDialogs, cleanupInvoice, parseMoney } = require('../helpers/app')
/** Legt einen Rechnungsentwurf mit einer Position an und liefert die ID. */
async function entwurfAnlegen(page, { tag, einzelpreis = '100.00', ustSatz = '19' }) {
await gotoOk(page, '/invoices/new')
// Ersten echten Kunden wählen (Index 0 ist der Platzhalter).
const kundenOptionen = page.locator('#customer_id option')
const anzahl = await kundenOptionen.count()
expect(anzahl, 'Kein Kunde vorhanden — die Instanz braucht mindestens einen Kunden').toBeGreaterThan(1)
const kundenValue = await kundenOptionen.nth(1).getAttribute('value')
await page.selectOption('#customer_id', kundenValue)
await page.fill('#betreff', tag)
// Position über den "+"-Button und das Popup hinzufügen.
await page.click('button[onclick^="showAddLineDialog"]')
await page.click('#add-line-popup >> text=Neue Position')
const zeile = page.locator('#invoice-lines-body tr').last()
await zeile.locator('textarea[name$="[bezeichnung]"]').fill(`Leistung ${tag}`)
await zeile.locator('input[name$="[menge]"]').fill('1')
await zeile.locator('select[name$="[ust_satz]"]').selectOption(ustSatz)
await zeile.locator('input[name$="[einzelpreis]"]').fill(einzelpreis)
await Promise.all([
page.waitForURL(/\/invoices\/\d+/),
page.click('button[type="submit"]:has-text("Entwurf speichern")'),
])
const id = Number(page.url().match(/\/invoices\/(\d+)/)?.[1] || 0)
expect(id, `Rechnungs-ID nicht ermittelbar: ${page.url()}`).toBeGreaterThan(0)
return id
}
test.describe('Rechnung: Anlegen, Finalisieren, Export', () => {
let invoiceId = 0
test.afterEach(async ({ page }) => {
await cleanupInvoice(page, invoiceId)
invoiceId = 0
})
test('Entwurf wird mit Position und Summen gespeichert', async ({ page }) => {
const tag = uniqueTag()
invoiceId = await entwurfAnlegen(page, { tag })
await expect(page.locator('body')).toContainText(tag)
await expect(page.locator('body')).toContainText('Entwurf')
// 100,00 netto + 19 % = 119,00 brutto müssen irgendwo auf der Seite stehen.
await expect(page.locator('body')).toContainText('119,00')
})
test('XML-Export eines Entwurfs liefert wohlgeformtes XRechnung-XML', async ({ page }) => {
const tag = uniqueTag()
invoiceId = await entwurfAnlegen(page, { tag })
const response = await page.request.get(`/invoices/${invoiceId}/xml`)
expect(response.status()).toBe(200)
expect(response.headers()['content-type']).toContain('xml')
const xml = await response.text()
expect(xml.startsWith('<?xml'), 'Antwort ist kein XML-Dokument').toBe(true)
// Root-Element der CII-Syntax, die XRechnung 3.0 verwendet.
expect(xml).toContain('CrossIndustryInvoice')
// Kein unaufgelöstes PHP und keine Fehlerseite im Download.
expect(xml).not.toContain('<?php')
expect(xml).not.toContain('Ein Fehler ist aufgetreten')
})
test('Finalisieren vergibt eine Nummer und liefert PDF und XML aus', async ({ page }) => {
const tag = uniqueTag()
invoiceId = await entwurfAnlegen(page, { tag })
acceptDialogs(page)
await page.click('button[type="submit"]:has-text("Finalisieren")')
await page.waitForLoadState('networkidle')
// Status darf nicht mehr "Entwurf" sein und es muss eine Nummer geben.
await expect(page.locator('body')).not.toContainText('Finalisieren & Nummerieren')
const seitentext = await page.locator('body').textContent()
expect(seitentext).toContain(tag)
// ── PDF ──
const pdf = await page.request.get(`/invoices/${invoiceId}/pdf`)
expect(pdf.status()).toBe(200)
const pdfBody = await pdf.body()
expect(pdfBody.subarray(0, 5).toString('latin1'), 'Kein gültiger PDF-Header').toBe('%PDF-')
expect(pdfBody.length).toBeGreaterThan(1000)
// ── XML ──
const xmlResponse = await page.request.get(`/invoices/${invoiceId}/xml`)
expect(xmlResponse.status()).toBe(200)
const xml = await xmlResponse.text()
expect(xml).toContain('CrossIndustryInvoice')
// Die vergebene Rechnungsnummer muss im XML stehen (BT-1).
const dateiname = xmlResponse.headers()['content-disposition'] || ''
const nummer = dateiname.match(/Rechnung-(.+)\.xml/)?.[1]
expect(nummer, `Rechnungsnummer nicht aus Dateiname ermittelbar: ${dateiname}`).toBeTruthy()
expect(nummer).not.toBe('Entwurf')
expect(xml).toContain(nummer)
})
test('PDF eines Entwurfs wird nicht ausgeliefert', async ({ page }) => {
const tag = uniqueTag()
invoiceId = await entwurfAnlegen(page, { tag })
// Im Entwurf gibt es bewusst keinen PDF-Link (GoBD: erst ab Finalisierung).
await expect(page.locator(`a[href="/invoices/${invoiceId}/pdf"]`)).toHaveCount(0)
})
})