import React from 'react'; import { describe, it, expect, afterEach, vi } from 'vitest'; import { render, fireEvent } from '@testing-library/react'; import { OutlinerBlock, mapRenderedOffsetToRaw } from '../OutlinerBlock'; import { DocumentBlock } from '../../../lib/types'; describe('mapRenderedOffsetToRaw', () => { it('is the identity for plain text', () => { expect(mapRenderedOffsetToRaw('Hallo Welt', 'Hallo Welt', 4)).toBe(4); }); it('skips bold markers', () => { expect(mapRenderedOffsetToRaw('Hallo **Welt** du', 'Hallo Welt du', 8)).toBe(10); }); it('maps behind closing markers', () => { expect(mapRenderedOffsetToRaw('a **b** c', 'a b c', 4)).toBe(8); }); it('maps into a link label', () => { expect(mapRenderedOffsetToRaw('x [Seite](https://a.de) y', 'x Seite y', 4)).toBe(5); }); it('handles the empty placeholder', () => { expect(mapRenderedOffsetToRaw('', '​', 1)).toBe(0); }); }); describe('clicking into an unfocused bullet', () => { afterEach(() => { delete (document as any).caretRangeFromPoint; }); it('places the caret at the click position instead of the end', () => { const block = { id: 'b1', type: 'bullet', content: 'Hallo **Welt** du', level: 0, position: 1 } as DocumentBlock; const { container } = render( {}} onCreateBlockBelow={() => {}} onDeleteBlock={() => {}} onIndentBlock={() => {}} onNavigateFocus={() => {}} registerTextareaRef={() => {}} /> ); const ta = container.querySelector('textarea') as HTMLTextAreaElement; const strong = container.querySelector('strong') as HTMLElement; // rendered "Welt" // jsdom has no layout: pretend the click hit between "We" and "lt" (document as any).caretRangeFromPoint = () => { const r = document.createRange(); r.setStart(strong.firstChild!, 2); return r; }; fireEvent.click(strong); expect(document.activeElement).toBe(ta); expect(ta.selectionStart).toBe(10); // "Hallo **We|lt** du" }); }); function renderBlock(block: DocumentBlock, extra: Record = {}) { return render( {}} onCreateBlockBelow={() => {}} onDeleteBlock={() => {}} onIndentBlock={() => {}} onNavigateFocus={() => {}} registerTextareaRef={() => {}} {...extra} /> ); } describe('undo shortcut', () => { it('saves pending typing first, then undoes', () => { const calls: string[] = []; const block = { id: 'b1', type: 'bullet', content: 'Alt', level: 0 } as DocumentBlock; const { container } = renderBlock(block, { onUpdateBlock: (b: DocumentBlock) => calls.push(`update:${b.content}`), onUndo: () => calls.push('undo'), onRedo: () => calls.push('redo'), }); const ta = container.querySelector('textarea') as HTMLTextAreaElement; ta.focus(); fireEvent.change(ta, { target: { value: 'Alt neu' } }); fireEvent.keyDown(ta, { key: 'z', ctrlKey: true }); fireEvent.keyDown(ta, { key: 'z', ctrlKey: true, shiftKey: true }); fireEvent.keyDown(ta, { key: 'y', metaKey: true }); expect(calls).toEqual(['update:Alt neu', 'undo', 'redo', 'redo']); }); }); describe('conflict copies', () => { it('show who lost and offer accept/discard', () => { const onResolve = vi.fn(); const block = { id: 'cf', type: 'bullet', content: 'Meine Version', level: 0, conflictOf: 'b1', conflictAuthor: 'Ben' } as DocumentBlock; const { getByText } = renderBlock(block, { onResolveConflict: onResolve }); expect(getByText(/Version von Ben/)).toBeTruthy(); fireEvent.click(getByText('Übernehmen')); fireEvent.click(getByText('Verwerfen')); expect(onResolve.mock.calls).toEqual([ ['cf', true], ['cf', false], ]); }); });