781bc9f9ee
NoteLink ersetzt <a> der Markdown-Vorschau: target=_blank, rel=noopener noreferrer; #-Sprungmarken bleiben im Fenster. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
173 lines
6.5 KiB
TypeScript
173 lines
6.5 KiB
TypeScript
import { render } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import MDEditor from '@uiw/react-md-editor';
|
|
import rehypeSanitize from 'rehype-sanitize';
|
|
import { NoteCheckbox, NoteLink, 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('NoteCheckbox — bedient sich selbst (quick-260921-jt4, D-01)', () => {
|
|
it('ist nicht mehr readOnly und traegt kein disabled', () => {
|
|
const { container } = render(<NoteCheckbox checked={true} />);
|
|
const box = container.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
|
|
expect(box.readOnly).toBe(false);
|
|
expect(box.disabled).toBe(false);
|
|
});
|
|
|
|
it('ruft onToggle mit dem eigenen DOM-Element auf, wenn eine echte Tastaturbetaetigung (Leertaste) das fokussierte Kaestchen ausloest', async () => {
|
|
const onToggle = vi.fn();
|
|
const user = userEvent.setup({ delay: null });
|
|
const { container } = render(
|
|
<NoteCheckbox checked={false} onToggle={onToggle} />,
|
|
);
|
|
const box = container.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
|
|
box.focus();
|
|
await user.keyboard(' ');
|
|
|
|
expect(onToggle).toHaveBeenCalledOnce();
|
|
expect(onToggle).toHaveBeenCalledWith(box);
|
|
});
|
|
|
|
it('ruft onToggle auch bei einem echten Mausklick auf', async () => {
|
|
const onToggle = vi.fn();
|
|
const user = userEvent.setup({ delay: null });
|
|
const { container } = render(
|
|
<NoteCheckbox checked={false} onToggle={onToggle} />,
|
|
);
|
|
const box = container.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
|
|
await user.click(box);
|
|
|
|
expect(onToggle).toHaveBeenCalledOnce();
|
|
expect(onToggle).toHaveBeenCalledWith(box);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
it('oeffnet Links in einem neuen Tab, Sprungmarken im selben Fenster', () => {
|
|
const SRC =
|
|
'[Doku](https://example.com/doku) und https://example.org\n\n[oben](#oben)';
|
|
|
|
const { container } = render(
|
|
<MDEditor.Markdown
|
|
source={SRC}
|
|
rehypePlugins={[[rehypeSanitize]]}
|
|
components={{ a: NoteLink }}
|
|
/>,
|
|
);
|
|
|
|
const doku = container.querySelector('a[href="https://example.com/doku"]');
|
|
const auto = container.querySelector('a[href="https://example.org"]');
|
|
const hash = container.querySelector('a[href="#oben"]');
|
|
|
|
for (const link of [doku, auto]) {
|
|
expect(link?.getAttribute('target')).toBe('_blank');
|
|
expect(link?.getAttribute('rel')).toBe('noopener noreferrer');
|
|
expect(link?.hasAttribute('node')).toBe(false);
|
|
}
|
|
expect(hash).not.toBeNull();
|
|
expect(hash?.hasAttribute('target')).toBe(false);
|
|
});
|
|
});
|
|
});
|