Neues static/sort.js sortiert Tabellen clientseitig per Klick auf den Spaltenkopf (⇅/▲/▼): Datum TT.MM.JJJJ, deutsche Zahlen/Beträge, sonst Text (Intl.Collator 'de'). Sortierwert optional per td[data-sort] (Punkt-Dezimal/ISO) überschreibbar, th[data-nosort] bekommt keinen Button, table[data-sort-mode="server"] wird übersprungen (Buchungen sortiert serverseitig, folgt in Task 5). Bearbeiten-/Tilgungsplan- Zeilen (tr[data-sort-follow]) wandern mit ihrer Vorzeile mit. Initialisierung über htmx.onLoad, greift auch bei nachgeladenen Fragmenten. Templates entsprechend annotiert (Planung, Szenarien, Admin, Index, Salden, Import-Liste, Buchungen); Salden-Gesamtzeile dafür in ein tfoot verschoben. test_gui.py::test_gui_zeigt_deutsche_betragsformate angepasst, da das neue machinenlesbare data-sort-Attribut bewusst Punkt-Dezimal führt (Interface-Konvention) - die Prüfung auf deutsches Format im sichtbaren Text bleibt bestehen. JS-Unit-/DOM-Tests unter tests/js (node --test, DOM-Teil optional via jsdom), pytest-Wrapper skip ohne node (Fable-Gate führt sie per Podman aus). Suite: 248 passed, 3 skipped (vorher 242/2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
61 lines
2.6 KiB
JavaScript
61 lines
2.6 KiB
JavaScript
'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/sort.js'), 'utf8');
|
||
|
||
function load(html) {
|
||
const dom = new JSDOM(`<!doctype html><body>${html}</body>`, { runScripts: 'outside-only' });
|
||
dom.window.eval(SRC);
|
||
dom.window.FBSort.init(dom.window.document.body);
|
||
return dom.window.document;
|
||
}
|
||
|
||
const TABLE = `
|
||
<table id="t"><thead><tr><th>Name</th><th>Betrag</th><th>Aktion</th><th></th></tr></thead>
|
||
<tbody>
|
||
<tr id="r1"><td>B</td><td>10,00 €</td><td>x</td><td></td></tr>
|
||
<tr id="r1-edit" hidden data-sort-follow><td colspan="4">edit1</td></tr>
|
||
<tr id="r2"><td>A</td><td>-5,00 €</td><td>x</td><td></td></tr>
|
||
<tr id="r2-edit" hidden data-sort-follow><td colspan="4">edit2</td></tr>
|
||
<tr id="r3"><td>C</td><td>–</td><td>x</td><td></td></tr>
|
||
</tbody>
|
||
<tfoot><tr id="sum"><td>Gesamt</td><td>5,00 €</td><td></td><td></td></tr></tfoot>
|
||
</table>
|
||
<table data-sort-mode="server" id="s"><thead><tr><th>Datum</th></tr></thead><tbody></tbody></table>`;
|
||
|
||
const ids = (doc) => [...doc.querySelectorAll('#t tbody tr')].map((r) => r.id);
|
||
|
||
test('Buttons nur an Spalten mit Text, nicht an server-Tabellen', { skip: !JSDOM }, () => {
|
||
const doc = load(TABLE.replace('<th>Aktion</th>', '<th data-nosort>Aktion</th>'));
|
||
assert.equal(doc.querySelectorAll('#t .sort-btn').length, 2);
|
||
assert.equal(doc.querySelectorAll('#s .sort-btn').length, 0);
|
||
assert.equal(doc.querySelector('#t .sort-btn').textContent, '⇅');
|
||
});
|
||
|
||
test('Klick sortiert auf/ab, Folgezeilen wandern mit, Leerwert am Ende, tfoot bleibt', { skip: !JSDOM }, () => {
|
||
const doc = load(TABLE);
|
||
const btn = doc.querySelectorAll('#t .sort-btn')[1]; // Betrag
|
||
btn.click();
|
||
assert.equal(btn.textContent, '▲');
|
||
assert.deepEqual(ids(doc), ['r2', 'r2-edit', 'r1', 'r1-edit', 'r3']);
|
||
btn.click();
|
||
assert.equal(btn.textContent, '▼');
|
||
assert.deepEqual(ids(doc), ['r1', 'r1-edit', 'r2', 'r2-edit', 'r3']);
|
||
assert.equal(doc.querySelector('#t tfoot tr').id, 'sum');
|
||
const nameBtn = doc.querySelectorAll('#t .sort-btn')[0];
|
||
nameBtn.click();
|
||
assert.equal(btn.textContent, '⇅');
|
||
assert.deepEqual(ids(doc), ['r2', 'r2-edit', 'r1', 'r1-edit', 'r3']);
|
||
});
|
||
|
||
test('init zweimal erzeugt keine doppelten Buttons', { skip: !JSDOM }, () => {
|
||
const doc = load(TABLE);
|
||
doc.defaultView.FBSort.init(doc.body);
|
||
assert.equal(doc.querySelectorAll('#t .sort-btn').length, 3);
|
||
});
|