Steuerkram/tests/e2e/specs/beleg-buchung.spec.js

199 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-check
// Hotspot: Beleg-Upload → Buchungsmaske → gespeicherte Buchung.
//
// Der Netto/Brutto-Umschalter (Issue #137) ist reines JavaScript: Im Brutto-Modus
// steht im Feld `betrag_netto` ein Bruttowert, den ein submit-Handler kurz vor dem
// Absenden in den Nettobetrag umrechnet (src/Views/belege/detail.php).
// Ohne Browser ist dieser Pfad nicht prüfbar — ein PHP-Test sieht immer nur den
// bereits umgerechneten Wert und würde eine kaputte Umrechnung nie bemerken.
const { test, expect } = require('@playwright/test')
const { uploadBeleg, cleanupBeleg, gotoOk, parseMoney } = require('../helpers/app')
const KATEGORIE_KONTO = '4930' // Bürobedarf (Betriebsausgabe)
const ZAHLUNGS_KONTO = '1200' // Bank
/** Wählt im Konto-Select die Option mit der gegebenen Kontonummer. */
async function waehleKonto(page, selectId, kontonummer) {
const option = page.locator(`#${selectId} option`).filter({ hasText: new RegExp(`^\\s*${kontonummer}\\s+–`) }).first()
await expect(option, `Konto ${kontonummer} nicht in #${selectId} gefunden`).toHaveCount(1)
const value = await option.getAttribute('value')
await page.selectOption(`#${selectId}`, value)
return value
}
/**
* Füllt die Buchungsmaske bis auf den Betrag aus.
*
* Reihenfolge beachten: Die Auswahl des Kategorie-Kontos ruft updateAutoUst()
* auf und setzt den USt-Satz passend zum Steuercode des Kontos. Der Satz muss
* deshalb nach der Kontoauswahl gesetzt werden — sonst wird er überschrieben.
*/
async function buchungsmaskeVorbereiten(page, { ustSatz = '19', buchungstext }) {
await page.check('input[name="buchungstyp"][value="Ausgabe"]')
await waehleKonto(page, 'kategorie_konto_id', KATEGORIE_KONTO)
await waehleKonto(page, 'zahlungskonto_id', ZAHLUNGS_KONTO)
await page.selectOption('#ust_satz_select', ustSatz)
await page.fill('input[name="buchungstext"]', buchungstext)
}
test.describe('Beleg-Upload und Buchung', () => {
let belegId = 0
test.afterEach(async ({ page }) => {
await cleanupBeleg(page, belegId)
belegId = 0
})
test('Beleg wird hochgeladen und erscheint in der Belegliste', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await expect(page.locator('body')).toContainText(tag)
await gotoOk(page, '/belege')
await expect(page.locator('body')).toContainText(tag)
})
test('Buchung im NETTO-Modus speichert den eingegebenen Wert unverändert', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { buchungstext: tag })
// Netto ist der Default-Modus.
await expect(page.locator('#betrag_modus_netto_btn')).toHaveAttribute('aria-pressed', 'true')
await page.fill('#betrag_netto_input', '100.00')
// Live-Anzeige rechnet vorwärts: Netto 100 → USt 19 → Brutto 119.
await expect(page.locator('#ust_brutto_anzeige')).toContainText('19,00')
await expect(page.locator('#ust_brutto_anzeige')).toContainText('119,00')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
expect(parseMoney(await zeile.locator('td').nth(3).textContent())).toBeCloseTo(100.0, 2)
})
test('Buchung im BRUTTO-Modus wird vor dem Absenden in Netto umgerechnet', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { buchungstext: tag })
await page.click('#betrag_modus_brutto_btn')
await expect(page.locator('#betrag_modus_brutto_btn')).toHaveAttribute('aria-pressed', 'true')
await expect(page.locator('#betrag_modus_netto_btn')).toHaveAttribute('aria-pressed', 'false')
// 119,00 brutto bei 19 % → 100,00 netto.
await page.fill('#betrag_netto_input', '119.00')
await expect(page.locator('#ust_brutto_anzeige')).toContainText('100,00')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile, 'Buchung wurde nicht gespeichert').toHaveCount(1)
// Kern der Regression: Gespeichert werden muss der Nettobetrag 100,00 —
// nicht die eingegebenen 119,00.
const gespeichert = parseMoney(await zeile.locator('td').nth(3).textContent())
expect(gespeichert, 'Bruttoeingabe wurde nicht in Netto umgerechnet').toBeCloseTo(100.0, 2)
})
test('Moduswechsel rechnet den bereits eingegebenen Betrag um', async ({ page }) => {
const { id } = await uploadBeleg(page)
belegId = id
await page.selectOption('#ust_satz_select', '19')
await page.fill('#betrag_netto_input', '100.00')
// Netto → Brutto
await page.click('#betrag_modus_brutto_btn')
await expect(page.locator('#betrag_netto_input')).toHaveValue('119.00')
// und wieder zurück
await page.click('#betrag_modus_netto_btn')
await expect(page.locator('#betrag_netto_input')).toHaveValue('100.00')
})
test('Buchung mit 7 % USt rechnet im Brutto-Modus korrekt', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { ustSatz: '7', buchungstext: tag })
await page.click('#betrag_modus_brutto_btn')
await page.fill('#betrag_netto_input', '107.00')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
expect(parseMoney(await zeile.locator('td').nth(3).textContent())).toBeCloseTo(100.0, 2)
})
// Issue #166: Buchungsnummer, Buchungsdatum und Typ teilen sich eine Spalte,
// die Spalte „Anlage" entfällt — damit die Tabelle ohne waagerechtes Scrollen
// lesbar bleibt. Prüfbar nur am gerenderten DOM: die Zellaufteilung existiert
// nur in der View.
test('Buchungsnummer, Datum und Typ stehen in einer gemeinsamen Spalte', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { buchungstext: tag })
await page.fill('#betrag_netto_input', '100.00')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const zeile = page.locator('table tbody tr', { hasText: tag }).first()
await expect(zeile).toHaveCount(1)
// Erste Zelle trägt alle drei Angaben: Buchungsnummer (BU-JAHR-NNNNNN),
// Datum und Buchungstyp.
const ersteZelle = await zeile.locator('td').nth(0).textContent()
expect(ersteZelle).toMatch(/BU-\d{4}-\d+/)
expect(ersteZelle).toMatch(/\d{2}\.\d{2}\.\d{4}/)
expect(ersteZelle).toMatch(/Einnahme|Ausgabe/)
// Die Kopfzeile führt alle drei Bezeichnungen in einer einzigen Spalte;
// „Anlage" gibt es nicht mehr.
const kopf = page.locator('table thead tr').first()
const ersteKopfzelle = await kopf.locator('th').nth(0).textContent()
expect(ersteKopfzelle).toContain('Buchungsnr.')
expect(ersteKopfzelle).toContain('Datum')
expect(ersteKopfzelle).toContain('Typ')
await expect(kopf.locator('th')).toHaveCount(6)
await expect(kopf.locator('th', { hasText: 'Anlage' })).toHaveCount(0)
})
// Der Betrag darf nicht zwischen Zahl und Währungszeichen umbrechen.
test('Betrag und Währungszeichen bleiben zusammen', async ({ page }) => {
const { id, tag } = await uploadBeleg(page)
belegId = id
await buchungsmaskeVorbereiten(page, { buchungstext: tag })
await page.fill('#betrag_netto_input', '1234.56')
await page.click('#buchung_form button[type="submit"]')
await page.waitForLoadState('networkidle')
const wert = page.locator('table tbody tr', { hasText: tag }).first().locator('td').nth(3)
const text = await wert.textContent()
// Geschütztes Leerzeichen (U+00A0) statt eines gewöhnlichen Leerzeichens.
expect(text).toBe('1.234,56\u00A0€')
// Und der Betrag steht auch gerendert auf einer einzigen Zeile — ein
// Umbruch zwischen Zahl und Währungszeichen ergäbe zwei Rechtecke.
const fragmente = await wert.evaluate((el) => {
const r = document.createRange()
r.selectNodeContents(el)
return r.getClientRects().length
})
expect(fragmente).toBe(1)
})
// Hinweis: Dass ein Beleg mit Buchungen nicht löschbar ist, ist reine
// Model-Logik (Beleg::delete) und wird in tests/77_BelegLoeschsperreTest.php
// geprüft — dort schneller, isoliert und ohne Browser.
})