feat: Warnfenster und Planungs-Hinweis bei möglicher Doppelzählung (A12-T4)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
let JSDOM = null;
|
||||
try { ({ JSDOM } = require('jsdom')); } catch (e) { /* jsdom fehlt -> skip */ }
|
||||
const SRC = fs.readFileSync(path.join(__dirname, '../../app/static/doppelzaehlung.js'), 'utf8');
|
||||
|
||||
const HTML = `
|
||||
<form id="f" hx-post="/api/recurring"><input name="name" value="Rate Auto"></form>
|
||||
<dialog id="doppelzaehlung-dialog">
|
||||
<p class="dz-meldung"></p><ul class="dz-konflikte"></ul>
|
||||
<button type="button" id="dz-abbrechen">Abbrechen</button>
|
||||
<button type="button" id="dz-bestaetigen">Trotzdem speichern</button>
|
||||
</dialog>`;
|
||||
const BODY = JSON.stringify({ detail: { code: 'doppelzaehlung_kredit', meldung: 'Erklärung',
|
||||
konflikte: [{ posten: 'Rate Auto', posten_betrag: '-100.00', kredit: 'Auto', kredit_rate: '100.00' }] } });
|
||||
|
||||
function load() {
|
||||
const dom = new JSDOM(`<!doctype html><body>${HTML}</body>`, { runScripts: 'outside-only' });
|
||||
const w = dom.window;
|
||||
const calls = [];
|
||||
w.htmx = { trigger: (el, name) => calls.push([el.id, name]) };
|
||||
const dlg = w.document.getElementById('doppelzaehlung-dialog');
|
||||
dlg.showModal = function () { this.setAttribute('open', ''); };
|
||||
dlg.close = function () { this.removeAttribute('open'); };
|
||||
w.eval(SRC);
|
||||
w.FBDoppel.init(w.document);
|
||||
const form = w.document.getElementById('f');
|
||||
const evt = { detail: { xhr: { status: 409, responseText: BODY }, elt: form } };
|
||||
return { w, dlg, form, evt, calls };
|
||||
}
|
||||
|
||||
test('409 öffnet Dialog mit Erklärung und Konfliktliste', { skip: !JSDOM }, () => {
|
||||
const { w, dlg, evt } = load();
|
||||
assert.equal(w.FBDoppel.handle(evt), true);
|
||||
assert.ok(dlg.hasAttribute('open'));
|
||||
assert.equal(dlg.querySelector('.dz-meldung').textContent, 'Erklärung');
|
||||
assert.equal(dlg.querySelectorAll('.dz-konflikte li').length, 1);
|
||||
});
|
||||
|
||||
test('andere Fehler werden nicht abgefangen', { skip: !JSDOM }, () => {
|
||||
const { w, form } = load();
|
||||
assert.equal(w.FBDoppel.handle({ detail: { xhr: { status: 422, responseText: '{}' }, elt: form } }), false);
|
||||
});
|
||||
|
||||
test('Abbrechen speichert nicht', { skip: !JSDOM }, () => {
|
||||
const { w, dlg, form, evt, calls } = load();
|
||||
w.FBDoppel.handle(evt);
|
||||
w.document.getElementById('dz-abbrechen').click();
|
||||
assert.ok(!dlg.hasAttribute('open'));
|
||||
assert.equal(calls.length, 0);
|
||||
assert.ok(!form.hasAttribute('data-doppelzaehlung-bestaetigt'));
|
||||
assert.equal(form.querySelector('input').value, 'Rate Auto');
|
||||
});
|
||||
|
||||
test('Trotzdem speichern sendet erneut mit Flag', { skip: !JSDOM }, () => {
|
||||
const { w, dlg, form, evt, calls } = load();
|
||||
w.FBDoppel.handle(evt);
|
||||
w.document.getElementById('dz-bestaetigen').click();
|
||||
assert.ok(!dlg.hasAttribute('open'));
|
||||
assert.equal(form.getAttribute('data-doppelzaehlung-bestaetigt'), '1');
|
||||
assert.deepEqual(calls, [['f', 'submit']]);
|
||||
});
|
||||
|
||||
test('Flag nach Request entfernt', { skip: !JSDOM }, () => {
|
||||
const { w, form, evt } = load();
|
||||
w.FBDoppel.handle(evt);
|
||||
w.document.getElementById('dz-bestaetigen').click();
|
||||
const e = new w.CustomEvent('htmx:afterRequest', { bubbles: true, detail: { elt: form } });
|
||||
form.dispatchEvent(e);
|
||||
assert.ok(!form.hasAttribute('data-doppelzaehlung-bestaetigt'));
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const D = require('../../app/static/doppelzaehlung.js');
|
||||
|
||||
const body = { detail: { code: 'doppelzaehlung_kredit', meldung: 'M',
|
||||
konflikte: [{ posten: 'Rate Auto', posten_betrag: '-1234.50', kredit: 'Auto', kredit_rate: '1234.50' }] } };
|
||||
|
||||
test('parseDetail erkennt nur 409 mit Code', () => {
|
||||
assert.equal(D.parseDetail({ status: 409, responseText: JSON.stringify(body) }).code, 'doppelzaehlung_kredit');
|
||||
assert.equal(D.parseDetail({ status: 409, responseText: '{"detail":"Kategorie existiert bereits"}' }), null);
|
||||
assert.equal(D.parseDetail({ status: 422, responseText: JSON.stringify(body) }), null);
|
||||
assert.equal(D.parseDetail({ status: 409, responseText: 'kein json' }), null);
|
||||
assert.equal(D.parseDetail(null), null);
|
||||
});
|
||||
|
||||
test('konfliktText deutsch formatiert', () => {
|
||||
assert.equal(D.konfliktText(body.detail.konflikte[0]),
|
||||
'Posten „Rate Auto“ (-1.234,50 €) ↔ Kredit „Auto“ (Rate 1.234,50 €)');
|
||||
});
|
||||
@@ -588,3 +588,46 @@ def test_planungs_refresh_fehler_bricht_seiten_nicht(client, monkeypatch):
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
for path in ("/", "/planung"):
|
||||
assert client.get(path).status_code == 200, path
|
||||
|
||||
|
||||
def test_base_hat_doppelzaehlungs_dialog(client):
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
html = client.get("/").text
|
||||
assert '<script src="/static/doppelzaehlung.js"></script>' in html
|
||||
assert '<dialog id="doppelzaehlung-dialog">' in html
|
||||
assert 'id="dz-abbrechen" autofocus' in html
|
||||
assert ">Trotzdem speichern</button>" in html
|
||||
assert "FBDoppel.handle(e)" in html
|
||||
assert "doppelzaehlung_bestaetigt = true" in html
|
||||
|
||||
|
||||
def test_planung_zeigt_doppelzaehlungs_hinweis(client, db):
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
db.add_all([
|
||||
Loan(name="Auto", principal=Decimal("1200.00"), annual_rate_pct=Decimal("0"),
|
||||
term_months=12, payout_date=date(2026, 1, 15), repayment_type="annuity"),
|
||||
RecurringItem(name="Rate Auto", amount=Decimal("-100.00"), rhythm="monthly", due_day=15),
|
||||
RecurringItem(name="Miete", amount=Decimal("-600.00"), rhythm="monthly", due_day=1),
|
||||
])
|
||||
db.commit()
|
||||
html = client.get("/planung").text
|
||||
assert "⚠ zählt evtl. doppelt mit Kredit „Auto“" in html
|
||||
assert "⚠ zählt evtl. doppelt mit Posten „Rate Auto“" in html
|
||||
assert html.count("⚠ zählt evtl. doppelt") == 2
|
||||
assert '<td data-sort="Rate Auto">' in html
|
||||
assert '<td data-sort="Auto">' in html
|
||||
|
||||
|
||||
def test_planung_ohne_konflikt_kein_hinweis(client, db):
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
db.add(RecurringItem(name="Miete", amount=Decimal("-600.00"), rhythm="monthly", due_day=1))
|
||||
db.commit()
|
||||
assert "zählt evtl. doppelt" not in client.get("/planung").text
|
||||
|
||||
|
||||
def test_vorschlag_formular_nutzt_json_form_post(client):
|
||||
# Review Focus 5: „Vorschlag übernehmen" läuft über denselben Warnweg.
|
||||
import re
|
||||
from pathlib import Path
|
||||
src = Path("app/templates/planning.html").read_text(encoding="utf-8")
|
||||
assert re.search(r'class="inline-form" hx-ext="json-form" hx-post="/api/recurring"', src)
|
||||
|
||||
Reference in New Issue
Block a user