diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewRailController.queued-positions.test.tsx b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.queued-positions.test.tsx new file mode 100644 index 00000000000..485b76efb5b --- /dev/null +++ b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.queued-positions.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment happy-dom +import { act, cleanup, renderHook } from '@testing-library/react' +import { Editor } from '@tiptap/core' +import StarterKit from '@tiptap/starter-kit' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import { useRichMarkdownReviewRailController } from './useRichMarkdownReviewRailController' + +const frames = new Map() +const editors: Editor[] = [] +let nextFrameId = 0 + +const comment: DiffComment = { + id: 'note', + worktreeId: 'workspace', + filePath: 'note.md', + source: 'markdown', + lineNumber: 1, + selectedText: 'selected', + body: 'Review note', + createdAt: 0, + side: 'modified' +} + +beforeEach(() => { + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { + const id = ++nextFrameId + frames.set(id, callback) + return id + }) + vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((id) => { + frames.delete(id) + }) +}) + +afterEach(() => { + cleanup() + for (const editor of editors.splice(0)) { + editor.destroy() + } + frames.clear() + vi.restoreAllMocks() +}) + +function makeEditor(): Editor { + const editor = new Editor({ + element: document.createElement('div'), + extensions: [StarterKit], + content: '

selected body

' + }) + editor.view.coordsAtPos = () => ({ left: 0, right: 1, top: 0, bottom: 1 }) + editors.push(editor) + return editor +} + +function flushFrames(): void { + const pending = [...frames.values()] + frames.clear() + act(() => { + for (const callback of pending) { + callback(performance.now()) + } + }) +} + +describe('review rail queued note positions', () => { + it('renders current notes when comments change before a queued frame', () => { + const initialProps = { + canAnnotateRichMarkdown: true, + content: 'selected body', + editorRef: { current: makeEditor() }, + markdownComments: [comment], + markdownSourceLineOffset: 0, + markdownSourceLineOffsetRef: { current: 0 }, + scrollContainerRef: { current: document.createElement('div') } + } + const rail = renderHook((props) => useRichMarkdownReviewRailController(props), { + initialProps + }) + act(() => rail.result.current.setReviewRailOpen(true)) + rail.rerender({ ...initialProps, markdownComments: [{ ...comment, id: 'latest-note' }] }) + flushFrames() + expect(rail.result.current.notePositions.map((position) => position.comment.id)).toEqual([ + 'latest-note' + ]) + }) +}) diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts index 4922e4e8011..5c705d1d8ac 100644 --- a/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts +++ b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts @@ -190,7 +190,11 @@ export function useRichMarkdownReviewRailController({ [editorRef, markdownSourceLineOffsetRef, pulseRichMarkdownSourceRange, scrollContainerRef] ) - useEffect(() => requestSyncNotePositions(), [content, markdownComments, requestSyncNotePositions]) + // Replace a queued frame so it measures with the current comments and offset. + useEffect(() => { + requestSyncNotePositions() + return cancelNotePositionFrame + }, [cancelNotePositionFrame, content, markdownComments, requestSyncNotePositions]) useEffect(() => { if (!reviewRailVisible) {