feat(web): Notiz-Widget — Aufgabenlisten in der Ansicht abhakbar

- Hilfsmodul note-task-list.tsx mit isTaskLine/toggleTaskLine (GFM-konforme
  Regex, Code-Zaeune werden uebersprungen) und NoteCheckbox (kein disabled)
- previewOptions des Notiz-Widgets reicht components.input = NoteCheckbox
  durch react-markdown nach rehypeSanitize durch (bleibt aktiv, T-IEX-01)
- Klick im Vorschau-Container kippt genau die N-te Aufgabenzeile und
  speichert sofort (verwirft einen laufenden Tipp-Entprell-Timer)
- Hinweis: note-task-list als .tsx statt .ts angelegt (Komponente braucht
  JSX) — Abweichung dokumentiert in SUMMARY.md
- 16 neue/erweiterte Tests gruen, tsc Exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 13:36:17 +02:00
parent 3c890afd92
commit 684f063a8e
4 changed files with 384 additions and 12 deletions
@@ -0,0 +1,107 @@
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import MDEditor from '@uiw/react-md-editor';
import rehypeSanitize from 'rehype-sanitize';
import { NoteCheckbox, isTaskLine, toggleTaskLine } from './note-task-list';
describe('note-task-list', () => {
describe('isTaskLine', () => {
it('erkennt gueltige Aufgabenzeilen', () => {
expect(isTaskLine('- [ ] Milch')).toBe(true);
expect(isTaskLine('- [x] Brot')).toBe(true);
expect(isTaskLine('* [X] Eier')).toBe(true);
expect(isTaskLine('+ [ ] Butter')).toBe(true);
expect(isTaskLine('1. [ ] Mehl')).toBe(true);
expect(isTaskLine('2) [x] Salz')).toBe(true);
expect(isTaskLine(' - [ ] eingerueckt')).toBe(true);
expect(isTaskLine('- [\t] Tab')).toBe(true);
});
it('lehnt Nicht-Aufgabenzeilen ab', () => {
expect(isTaskLine('- [ ]')).toBe(false);
expect(isTaskLine('- [ ]Text')).toBe(false);
expect(isTaskLine('- Milch')).toBe(false);
expect(isTaskLine('[ ] ohne Punkt')).toBe(false);
expect(isTaskLine('- [y] falsch')).toBe(false);
expect(isTaskLine('')).toBe(false);
});
});
describe('toggleTaskLine', () => {
it('kippt genau die Zeile am angegebenen Index auf [x]', () => {
const content = '- [ ] Milch\n- [ ] Brot\n- [ ] Eier';
expect(toggleTaskLine(content, 0)).toBe(
'- [x] Milch\n- [ ] Brot\n- [ ] Eier',
);
expect(toggleTaskLine(content, 2)).toBe(
'- [ ] Milch\n- [ ] Brot\n- [x] Eier',
);
});
it('kippt [x]/[X] zurueck auf [ ]', () => {
expect(toggleTaskLine('- [x] Brot', 0)).toBe('- [ ] Brot');
expect(toggleTaskLine('- [X] Brot', 0)).toBe('- [ ] Brot');
});
it('zaehlt Nicht-Aufgabenzeilen nicht mit, alle anderen Zeilen bleiben unveraendert', () => {
const content = '# Einkauf\n\nText\n- [ ] Milch\n- normal\n- [ ] Brot';
const result = toggleTaskLine(content, 1);
expect(result).toBe('# Einkauf\n\nText\n- [ ] Milch\n- normal\n- [x] Brot');
});
it('behaelt Einrueckung und Nummerierung bei', () => {
const content = '- [ ] A\n - [ ] B\n1. [ ] C';
expect(toggleTaskLine(content, 1)).toBe('- [ ] A\n - [x] B\n1. [ ] C');
expect(toggleTaskLine(content, 2)).toBe('- [ ] A\n - [ ] B\n1. [x] C');
});
it('gibt bei Index ausserhalb des Bereichs die unveraenderte Eingabe zurueck', () => {
const content = '- [ ] A\n- [ ] B\n- [ ] C';
expect(toggleTaskLine(content, -1)).toBe(content);
expect(toggleTaskLine(content, 3)).toBe(content);
expect(toggleTaskLine('', 0)).toBe('');
});
it('ueberspringt Zeilen innerhalb von Code-Zaeunen', () => {
const withBackticks = '```\n- [ ] nicht\n```\n- [ ] echt';
expect(toggleTaskLine(withBackticks, 0)).toBe(
'```\n- [ ] nicht\n```\n- [x] echt',
);
const withTilde = '~~~\n- [ ] nicht\n~~~\n- [ ] echt';
expect(toggleTaskLine(withTilde, 0)).toBe(
'~~~\n- [ ] nicht\n~~~\n- [x] echt',
);
});
});
describe('echte Vorschau (MDEditor.Markdown + rehypeSanitize + components-Override)', () => {
it('rendert genau die Aufgabenzeilen als anklickbare Kaestchen', () => {
const SRC =
'- [ ] eins\n- [x] zwei\n* [X] drei\n- [ ]\n- [ ]kein\n1. [ ] vier';
const { container } = render(
<MDEditor.Markdown
source={SRC}
rehypePlugins={[[rehypeSanitize]]}
components={{ input: NoteCheckbox }}
/>,
);
const boxes = container.querySelectorAll('input[type="checkbox"]');
const expectedCount = SRC.split('\n').filter(isTaskLine).length;
expect(expectedCount).toBe(4);
expect(boxes.length).toBe(4);
for (const box of Array.from(boxes)) {
expect((box as HTMLInputElement).disabled).toBe(false);
}
expect((boxes[0] as HTMLInputElement).checked).toBe(false);
expect((boxes[1] as HTMLInputElement).checked).toBe(true);
expect((boxes[2] as HTMLInputElement).checked).toBe(true);
expect((boxes[3] as HTMLInputElement).checked).toBe(false);
});
});
});