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>
127 lines
4.8 KiB
JavaScript
127 lines
4.8 KiB
JavaScript
// @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)
|
||
})
|
||
})
|