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:
@@ -12,23 +12,53 @@ vi.mock('next-intl', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock @uiw/react-md-editor — provide a minimal textarea-based substitute
|
||||
// Mock @uiw/react-md-editor — provide a minimal textarea-based substitute.
|
||||
// Im Modus 'preview' wird stattdessen ein Vorschau-Div gerendert, das fuer
|
||||
// jede Aufgabenzeile ein Kaestchen enthaelt (quick-260916-iex) — nutzt die
|
||||
// echte `components.input`-Komponente aus `previewOptions`, wenn vorhanden,
|
||||
// damit auch NoteCheckbox im Klickpfad landet.
|
||||
const TASK_MOCK_RE = /^\s*(?:[-*+]|\d+[.)])\s+\[([ xX])\]\s+\S/;
|
||||
|
||||
vi.mock('@uiw/react-md-editor', () => {
|
||||
const MDEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
preview,
|
||||
previewOptions,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
value?: string;
|
||||
onChange?: (val?: string) => void;
|
||||
preview?: string;
|
||||
previewOptions?: { components?: { input?: React.ComponentType<{ checked?: boolean }> } };
|
||||
'data-testid'?: string;
|
||||
}) => (
|
||||
<textarea
|
||||
data-testid={testId ?? 'md-editor'}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}) => {
|
||||
if (preview === 'preview') {
|
||||
const InputComponent = previewOptions?.components?.input;
|
||||
const lines = (value ?? '').split('\n');
|
||||
return (
|
||||
<div data-testid={testId ?? 'md-editor'}>
|
||||
{lines.map((line, i) => {
|
||||
const match = line.match(TASK_MOCK_RE);
|
||||
if (!match) return null;
|
||||
const checked = match[1] !== ' ';
|
||||
return InputComponent ? (
|
||||
<InputComponent key={i} checked={checked} />
|
||||
) : (
|
||||
<input key={i} type="checkbox" readOnly checked={checked} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<textarea
|
||||
data-testid={testId ?? 'md-editor'}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MDEditor.displayName = 'MDEditor';
|
||||
|
||||
return {
|
||||
@@ -171,4 +201,93 @@ describe('NoteWidget', () => {
|
||||
|
||||
abortSpy.mockRestore();
|
||||
});
|
||||
|
||||
// quick-260916-iex: Aufgabenlisten in der Ansicht abhakbar
|
||||
it('kippt beim Klick auf ein Kaestchen in der Ansicht genau diese Zeile und speichert sofort', async () => {
|
||||
render(
|
||||
<NoteWidget
|
||||
instanceId="note-1"
|
||||
config={{ content: '- [ ] Milch\n- [x] Brot\n- [ ] Eier', title: 'Einkauf' }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const boxes = screen.getAllByRole('checkbox');
|
||||
expect(boxes).toHaveLength(3);
|
||||
|
||||
fireEvent.click(boxes[2]);
|
||||
await act(async () => {});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toContain('/dashboard/widgets/note-1/config');
|
||||
expect(init.method).toBe('PATCH');
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
config: { content: '- [ ] Milch\n- [x] Brot\n- [x] Eier', title: 'Einkauf' },
|
||||
});
|
||||
|
||||
expect((screen.getAllByRole('checkbox')[2] as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it('kippt ein angehaktes Kaestchen beim Klick wieder ab', async () => {
|
||||
render(
|
||||
<NoteWidget
|
||||
instanceId="note-1"
|
||||
config={{ content: '- [ ] Milch\n- [x] Brot\n- [ ] Eier', title: 'Einkauf' }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const boxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(boxes[1]);
|
||||
await act(async () => {});
|
||||
|
||||
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
config: { content: '- [ ] Milch\n- [ ] Brot\n- [ ] Eier', title: 'Einkauf' },
|
||||
});
|
||||
});
|
||||
|
||||
it('verwirft einen noch laufenden Entprell-Timer aus dem Tippen beim Abhaken', async () => {
|
||||
render(
|
||||
<NoteWidget
|
||||
instanceId="note-1"
|
||||
config={{ content: '- [ ] Milch\n- [ ] Brot', title: 'Einkauf' }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Stift an: in den Bearbeitungsmodus wechseln und tippen
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
});
|
||||
const textarea = screen.getByTestId('md-editor') as HTMLTextAreaElement;
|
||||
fireEvent.change(textarea, { target: { value: '- [ ] Milch\n- [ ] Brot' } });
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Stift wieder aus: zurueck in die Ansicht
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
});
|
||||
|
||||
// Erstes Kaestchen anklicken -> sofortiges Speichern, alter Timer verworfen
|
||||
const boxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(boxes[0]);
|
||||
await act(async () => {});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
config: { content: '- [x] Milch\n- [ ] Brot', title: 'Einkauf' },
|
||||
});
|
||||
|
||||
// Der alte, eigentlich noch laufende Timer darf nichts mehr nachschieben
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(2000);
|
||||
});
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user