Finanzmanager/tests/e2e/specs/importexport.spec.js
fds2610-bot 2c86e8522b Import und Export in QIF und CSV, Housekeeping für Importdateien
Aus der Kachel QIF-Import wird Import/Export mit je einem Knopf für beide
Richtungen; davor steht die Wahl des Dateiformats (QIF oder CSV), die als
Dialog und ohne Skript als eigene Seite funktioniert.

- CsvBuchungsImport/CsvTabelle: Buchungen samt Splitzeilen, Spalten über die
  Kopfzeile, dieselbe Dublettenerkennung und der Trockenlauf wie beim QIF.
- BuchungsExport/ExportController: Filter nach Zeitraum, Konto, Kategorie und
  Klasse; die Datei entsteht im Speicher, ein Exportverzeichnis gibt es nicht.
- Housekeeping: Hochgeladene Importdateien und data/tmp werden nach der
  eingestellten Frist gelöscht (Vorgabe 14 Tage, 0 schaltet ab).

Migration 013, Test 29 sowie E2E-Spec importexport.spec.js.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:01:30 +02:00

127 lines
4.8 KiB
JavaScript
Raw Permalink 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
const { test, expect } = require('@playwright/test')
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
const MARKE = Date.now().toString().slice(-6)
async function anmelden(page) {
await page.goto('/login')
if (page.url().includes('/einrichtung')) {
throw new Error(
'Instanz ist noch nicht eingerichtet. Zuerst einen Benutzer anlegen: ' +
'docker compose exec -u www-data php php scripts/e2e-benutzer.php'
)
}
await page.fill('#benutzername', BENUTZER)
await page.fill('#passwort', PASSWORT)
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/$/)
}
// Formatwahl und Download laufen im Browser ab – der Dialog braucht das Skript,
// der Download den Antwortkopf. Beides lässt sich in der PHP-Suite nicht prüfen.
test.describe('Import und Export', () => {
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
test('Die Kachel fragt erst nach dem Format und öffnet dann den Dialog', async ({ page }) => {
await page.goto('/verwaltung')
const kachel = page.locator('.kachel', { hasText: 'Import/Export' })
await expect(kachel.getByRole('link', { name: 'Import', exact: true })).toBeVisible()
await expect(kachel.getByRole('link', { name: 'Export', exact: true })).toBeVisible()
// Vor dem Klick ist der Auswahldialog zu.
const dialog = page.locator('dialog#formatwahl')
await expect(dialog).toBeHidden()
await kachel.getByRole('link', { name: 'Export', exact: true }).click()
await expect(dialog).toBeVisible()
await expect(dialog.locator('h2')).toContainText('Export')
await dialog.getByRole('link', { name: 'CSV' }).click()
await expect(page).toHaveURL(/\/verwaltung\/export\?format=csv$/)
await expect(page.locator('h1')).toContainText('Export (CSV)')
// Derselbe Weg für den Import – der Dialog merkt sich die Richtung nicht.
await page.goto('/verwaltung')
await kachel.getByRole('link', { name: 'Import', exact: true }).click()
await expect(dialog.locator('h2')).toContainText('Import')
await dialog.getByRole('link', { name: 'QIF' }).click()
await expect(page).toHaveURL(/\/verwaltung\/import\?format=qif$/)
})
test('Ohne Skript führt derselbe Knopf auf die Auswahl als Seite', async ({ page }) => {
await page.goto('/verwaltung/export')
await expect(page.locator('h2').first()).toContainText('Format wählen')
await expect(page.locator('a[href="/verwaltung/export?format=qif"]')).toBeVisible()
await expect(page.locator('a[href="/verwaltung/export?format=csv"]')).toBeVisible()
})
test('Der gefilterte Export lädt eine Datei herunter', async ({ page }) => {
await page.goto('/verwaltung/export?format=csv')
await page.fill('input[name="von"]', '2026-01-01')
await page.fill('input[name="bis"]', '2026-12-31')
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'CSV herunterladen' }).click(),
])
expect(download.suggestedFilename()).toMatch(/^buchungen-2026-01-01-bis-2026-12-31\.csv$/)
// Die Kopfzeile muss stehen, sonst wäre es keine Buchungstabelle.
const strom = await download.createReadStream()
const teile = []
for await (const stueck of strom) {
teile.push(stueck)
}
expect(Buffer.concat(teile).toString('utf8')).toContain('Buchungstext')
})
test('Eine CSV lässt sich hochladen und im Trockenlauf prüfen', async ({ page }) => {
const dateiname = `e2e-buchungen-${MARKE}.csv`
const konto = `E2E CSV Konto ${MARKE}`
// Das Konto muss es geben – Konten legt der CSV-Import nicht an.
await page.goto('/konten/neu')
await page.fill('#name', konto)
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
const inhalt = [
'Art;Datum;Konto;Empfänger;Buchungstext;Betrag',
`Buchung;15.06.2026;${konto};E2E Laden;Testeinkauf ${MARKE};-12,34`,
'',
].join('\r\n')
await page.goto('/verwaltung/import?format=csv')
await page.setInputFiles('#datei', {
name: dateiname,
mimeType: 'text/csv',
buffer: Buffer.from(inhalt, 'utf8'),
})
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toContainText('hochgeladen')
await page.locator('tr', { hasText: dateiname })
.getByRole('button', { name: 'Trockenlauf' }).click()
await expect(page.locator('.kachel h2').first()).toContainText('Trockenlauf')
await expect(page.locator('tr', { hasText: 'Neu' }).first()).toContainText('1')
// Aufräumen – die Datei bliebe sonst bis zum Housekeeping liegen.
await page.goto('/verwaltung/import?format=csv')
page.on('dialog', async (dialog) => await dialog.accept())
await page.locator('tr', { hasText: dateiname })
.getByRole('button', { name: 'Löschen' }).click()
await expect(page.locator('body')).not.toContainText(dateiname)
})
})