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.
126 lines
5.2 KiB
JavaScript
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)
|
|
})
|
|
})
|