Reuse source-line calculations for Markdown review selections (#25173)

* Reuse source-line calculations for Markdown review selections

* Use typed editor probes in review selection performance coverage
This commit is contained in:
Neil
2026-10-04 13:19:34 -07:00
committed by GitHub
parent b32462f246
commit cbe64383dc
7 changed files with 524 additions and 98 deletions
@@ -0,0 +1,216 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Editor } from '@tiptap/core'
import type { DiffComment } from '../../../../shared/diff-comment-types'
import { createRichMarkdownExtensions } from './rich-markdown-extensions'
import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'
import {
buildRichMarkdownCommentBlocks,
getRichMarkdownCommentBlocks
} from './rich-markdown-comment-blocks'
import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html'
import {
createRichMarkdownHtmlSuperscriptLinkContext,
type RichMarkdownHtmlSuperscriptLinkContext
} from './rich-markdown-html-superscript-link-context'
import {
getRichMarkdownAnnotationHighlightRanges,
getRichMarkdownAnnotationHighlightRangesForComment,
getRichMarkdownAnnotationTarget,
getRichMarkdownCommentAtPos
} from './rich-markdown-review-annotations'
import { getRichMarkdownReviewRailBlocks } from './rich-markdown-review-rail-blocks'
const editors: Editor[] = []
const SOURCE = 'First paragraph\n\n```ts\none\n\ntwo\n```\n\nLast paragraph'
function createEditor(source = SOURCE, context?: RichMarkdownHtmlSuperscriptLinkContext) {
const root = document.createElement('div')
document.body.append(root)
const codec = createRichMarkdownEditorCodec()
const editor = new Editor({
element: root,
extensions: createRichMarkdownExtensions({
codec,
htmlSuperscriptLinks: Boolean(context),
htmlSuperscriptLinkContext: context
}),
content: encodeRawMarkdownHtmlForRichEditor(source, codec, {
htmlSuperscriptLinks: Boolean(context)
}),
contentType: 'markdown'
})
editors.push(editor)
editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false))
vi.spyOn(root, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 600, 400))
return { editor, root }
}
function selectLastParagraph(editor: Editor, characters = 4) {
let from: number | undefined
editor.state.doc.forEach((node, offset) => {
if (node.textContent === 'Last paragraph') {
from = offset + 1
}
})
if (from === undefined) {
throw new Error('Last paragraph missing')
}
editor.commands.setTextSelection({ from, to: from + characters })
const paragraph = Array.from(editor.view.dom.querySelectorAll('p')).find(
(element) => element.textContent === 'Last paragraph'
)
const text = paragraph?.firstChild
if (!(text instanceof Text)) {
throw new Error('Last paragraph text missing')
}
const range = document.createRange()
range.setStart(text, 0)
range.setEnd(text, characters)
window.getSelection()?.removeAllRanges()
window.getSelection()?.addRange(range)
return from
}
function comment(lineNumber = 29): DiffComment {
return {
id: 'note',
worktreeId: 'workspace',
filePath: 'notes.md',
source: 'markdown',
lineNumber,
selectedText: 'Last',
body: 'Review',
createdAt: 1,
side: 'modified'
}
}
afterEach(() => {
for (const editor of editors.splice(0)) {
editor.destroy()
}
window.getSelection()?.removeAllRanges()
document.body.replaceChildren()
vi.restoreAllMocks()
})
describe('Markdown review source block reuse', () => {
it('preserves source lines when the document interaction host changes', () => {
const context = createRichMarkdownHtmlSuperscriptLinkContext({
sourceFilePath: '/repo/notes.md',
worktreeId: 'workspace',
worktreeRoot: '/repo',
sourceOwner: { kind: 'local' }
})
const { editor } = createEditor(
'First <sup><a href="./source.md">[1]</a></sup>\n\nLast paragraph',
context
)
const blocks = getRichMarkdownCommentBlocks(editor)
const doc = editor.state.doc
const serialize = vi.spyOn(editor.markdown!, 'serialize')
const instrumentedBlocks = getRichMarkdownCommentBlocks(editor)
serialize.mockClear()
context.update({
sourceFilePath: '/remote/notes.md',
worktreeId: 'remote-workspace',
worktreeRoot: '/remote',
sourceOwner: { kind: 'ssh', connectionId: 'connection' }
})
expect(editor.state.doc).toBe(doc)
expect(getRichMarkdownCommentBlocks(editor)).toBe(instrumentedBlocks)
expect(serialize).not.toHaveBeenCalled()
serialize.mockRestore()
expect(buildRichMarkdownCommentBlocks(editor)).toEqual(blocks)
})
it('shares one source map across highlights, comment clicks, selection targets and the rail', () => {
const { editor, root } = createEditor()
const from = selectLastParagraph(editor)
vi.spyOn(Range.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(10, 20, 80, 20))
const serialize = vi.spyOn(editor.markdown!, 'serialize')
const json = vi.spyOn(editor, 'getJSON')
const note = comment()
const expected = [{ from, to: from + 4 }]
expect(getRichMarkdownAnnotationHighlightRanges(editor, [note], 20)).toEqual(expected)
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
expect(json).toHaveBeenCalledTimes(1)
serialize.mockClear()
json.mockClear()
for (let index = 0; index < 20; index++) {
expect(getRichMarkdownAnnotationHighlightRanges(editor, [note], 20)).toEqual(expected)
expect(getRichMarkdownAnnotationHighlightRangesForComment(editor, note, 20)).toEqual(expected)
expect(getRichMarkdownCommentAtPos(editor, [note], 20, from + 2)).toBe(note)
expect(getRichMarkdownAnnotationTarget(editor, root)).toMatchObject({
from,
to: from + 4,
selectedText: 'Last',
lineNumber: 9
})
expect(getRichMarkdownReviewRailBlocks(editor)).toBe(getRichMarkdownCommentBlocks(editor))
}
expect(serialize).not.toHaveBeenCalled()
expect(json).not.toHaveBeenCalled()
})
it('updates selected text and viewport geometry without rebuilding source lines', () => {
const { editor, root } = createEditor()
const rect = vi.spyOn(Range.prototype, 'getBoundingClientRect')
rect.mockReturnValue(new DOMRect(10, 20, 80, 20))
selectLastParagraph(editor)
const serialize = vi.spyOn(editor.markdown!, 'serialize')
expect(getRichMarkdownAnnotationTarget(editor, root)).toMatchObject({
selectedText: 'Last',
buttonTop: 48,
lineNumber: 9
})
serialize.mockClear()
const doc = editor.state.doc
selectLastParagraph(editor, 9)
rect.mockReturnValue(new DOMRect(10, 120, 140, 20))
expect(getRichMarkdownAnnotationTarget(editor, root)).toMatchObject({
selectedText: 'Last para',
buttonTop: 148,
lineNumber: 9
})
expect(editor.state.doc).toBe(doc)
expect(serialize).not.toHaveBeenCalled()
})
it('rebuilds source lines after editing a multiline block and undoing it', () => {
const { editor, root } = createEditor()
vi.spyOn(Range.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(10, 20, 80, 20))
selectLastParagraph(editor)
expect(getRichMarkdownAnnotationTarget(editor, root)?.lineNumber).toBe(9)
const serialize = vi.spyOn(editor.markdown!, 'serialize')
let codeFrom: number | undefined
editor.state.doc.forEach((node, offset) => {
if (node.type.name === 'codeBlock') {
codeFrom = offset + 1
}
})
if (codeFrom === undefined) {
throw new Error('Code block missing')
}
editor.view.dispatch(editor.state.tr.insertText('new\n', codeFrom))
selectLastParagraph(editor)
expect(getRichMarkdownAnnotationTarget(editor, root)?.lineNumber).toBe(10)
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
expect(
getRichMarkdownCommentAtPos(editor, [comment(30)], 20, editor.state.selection.from)
).toEqual(comment(30))
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
serialize.mockClear()
expect(editor.commands.undo()).toBe(true)
selectLastParagraph(editor)
expect(getRichMarkdownAnnotationTarget(editor, root)?.lineNumber).toBe(9)
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
serialize.mockClear()
expect(getRichMarkdownAnnotationTarget(editor, root)?.lineNumber).toBe(9)
expect(serialize).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,82 @@
import type { Editor, JSONContent } from '@tiptap/core'
import { countRichMarkdownReviewMarkdownLines } from './rich-markdown-review-line-count'
export type RichMarkdownCommentBlock = {
key: string
startLine: number
endLine: number
from: number
to: number
}
function serializeRichMarkdownJson(editor: Editor, content: JSONContent[]): string {
return (editor.markdown?.serialize({ type: 'doc', content }) ?? '').trimEnd()
}
export function buildRichMarkdownCommentBlocks(editor: Editor): RichMarkdownCommentBlock[] {
const jsonContent = editor.getJSON().content ?? []
const blocks: RichMarkdownCommentBlock[] = []
let nextLine = 1
let previousNodeJson: JSONContent | null = null
let previousNodeLineCount = 0
editor.state.doc.forEach((node, nodeOffset, index) => {
const nodeJson = jsonContent[index]
if (!nodeJson) {
return
}
const nodeMarkdown = serializeRichMarkdownJson(editor, [nodeJson])
const nodeLineCount = countRichMarkdownReviewMarkdownLines(nodeMarkdown)
if (previousNodeJson) {
const pairMarkdown = serializeRichMarkdownJson(editor, [previousNodeJson, nodeJson])
const separatorLineCount = Math.max(
0,
countRichMarkdownReviewMarkdownLines(pairMarkdown) - previousNodeLineCount - nodeLineCount
)
nextLine += separatorLineCount
}
const startLine = nextLine
const endLine = Math.max(startLine, startLine + nodeLineCount - 1)
const from = nodeOffset + 1
blocks.push({
key: `${index}:${startLine}-${endLine}`,
startLine,
endLine,
from,
to: from + Math.max(0, node.nodeSize - 1)
})
nextLine = endLine + 1
previousNodeJson = nodeJson
previousNodeLineCount = nodeLineCount
})
if (blocks.length === 0) {
blocks.push({ key: 'empty:1-1', startLine: 1, endLine: 1, from: 1, to: 1 })
}
return blocks
}
type RichMarkdownCommentBlocksSnapshot = {
doc: Editor['state']['doc']
markdown: Editor['markdown']
serialize: NonNullable<Editor['markdown']>['serialize'] | undefined
blocks: readonly RichMarkdownCommentBlock[]
}
// Keep only the current document per editor; scrolling changes geometry, not source lines.
const blocksByEditor = new WeakMap<Editor, RichMarkdownCommentBlocksSnapshot>()
export function getRichMarkdownCommentBlocks(editor: Editor): readonly RichMarkdownCommentBlock[] {
const doc = editor.state.doc
const markdown = editor.markdown
const serialize = markdown?.serialize
const cached = blocksByEditor.get(editor)
if (cached?.doc === doc && cached.markdown === markdown && cached.serialize === serialize) {
return cached.blocks
}
const blocks = buildRichMarkdownCommentBlocks(editor)
blocksByEditor.set(editor, { doc, markdown, serialize, blocks })
return blocks
}
@@ -1,6 +1,5 @@
import type { Dispatch, SetStateAction } from 'react'
import type { Editor } from '@tiptap/react'
import type { JSONContent } from '@tiptap/core'
import type { DiffComment } from '../../../../shared/diff-comment-types'
import type { RichMarkdownAnnotationHighlightRange } from './rich-markdown-annotation-highlight'
import {
@@ -10,7 +9,14 @@ import {
import type { RichMarkdownReviewNotePosition } from './rich-markdown-review-note-layout'
import { findRichMarkdownSelectedTextRanges } from './rich-markdown-review-text-ranges'
import { getRichMarkdownSelectionVisibleText } from './rich-markdown-visible-text-map'
import { countRichMarkdownReviewMarkdownLines } from './rich-markdown-review-line-count'
import {
getRichMarkdownCommentBlocks,
type RichMarkdownCommentBlock
} from './rich-markdown-comment-blocks'
export {
buildRichMarkdownCommentBlocks,
type RichMarkdownCommentBlock
} from './rich-markdown-comment-blocks'
export { countRichMarkdownReviewMarkdownLines } from './rich-markdown-review-line-count'
const RICH_MARKDOWN_ANNOTATION_BUTTON_SIZE_PX = 24
@@ -22,14 +28,6 @@ const RICH_MARKDOWN_ANNOTATION_POPOVER_WIDTH_PX = 420
const RICH_MARKDOWN_ANNOTATION_POPOVER_RIGHT_OFFSET_PX = 24
const RICH_MARKDOWN_ANNOTATION_POPOVER_MIN_HEIGHT_PX = 220
export type RichMarkdownCommentBlock = {
key: string
startLine: number
endLine: number
from: number
to: number
}
export type RichMarkdownComposerState = {
lineNumber: number
startLine?: number
@@ -45,54 +43,6 @@ export type RichMarkdownAnnotationTarget = RichMarkdownComposerState & {
buttonLeft: number
}
function serializeRichMarkdownJson(editor: Editor, content: JSONContent[]): string {
return (editor.markdown?.serialize({ type: 'doc', content }) ?? '').trimEnd()
}
export function buildRichMarkdownCommentBlocks(editor: Editor): RichMarkdownCommentBlock[] {
const jsonContent = editor.getJSON().content ?? []
const blocks: RichMarkdownCommentBlock[] = []
let nextLine = 1
let previousNodeJson: JSONContent | null = null
let previousNodeLineCount = 0
editor.state.doc.forEach((node, nodeOffset, index) => {
const nodeJson = jsonContent[index]
if (!nodeJson) {
return
}
const nodeMarkdown = serializeRichMarkdownJson(editor, [nodeJson])
const nodeLineCount = countRichMarkdownReviewMarkdownLines(nodeMarkdown)
if (previousNodeJson) {
const pairMarkdown = serializeRichMarkdownJson(editor, [previousNodeJson, nodeJson])
const separatorLineCount = Math.max(
0,
countRichMarkdownReviewMarkdownLines(pairMarkdown) - previousNodeLineCount - nodeLineCount
)
nextLine += separatorLineCount
}
const startLine = nextLine
const endLine = Math.max(startLine, startLine + nodeLineCount - 1)
const from = nodeOffset + 1
blocks.push({
key: `${index}:${startLine}-${endLine}`,
startLine,
endLine,
from,
to: from + Math.max(0, node.nodeSize - 1)
})
nextLine = endLine + 1
previousNodeJson = nodeJson
previousNodeLineCount = nodeLineCount
})
if (blocks.length === 0) {
blocks.push({ key: 'empty:1-1', startLine: 1, endLine: 1, from: 1, to: 1 })
}
return blocks
}
export function clampRichMarkdownAnnotationTarget(
editor: Editor,
target: RichMarkdownAnnotationTarget
@@ -122,8 +72,7 @@ export function getRichMarkdownAnnotationHighlightRanges(
if (comments.length === 0) {
return []
}
// Why once: block resolution re-serializes the doc; per comment it was O(n*doc).
const blocks = buildRichMarkdownCommentBlocks(editor)
const blocks = getRichMarkdownCommentBlocks(editor)
return comments.flatMap((comment) =>
getRichMarkdownAnnotationHighlightRangesForComment(
editor,
@@ -138,10 +87,9 @@ export function getRichMarkdownAnnotationHighlightRangesForComment(
editor: Editor,
comment: DiffComment,
markdownSourceLineOffset: number,
// Why optional: callers looping over comments pass one shared build.
prebuiltBlocks?: readonly RichMarkdownCommentBlock[]
): RichMarkdownAnnotationHighlightRange[] {
const blocks = prebuiltBlocks ?? buildRichMarkdownCommentBlocks(editor)
const blocks = prebuiltBlocks ?? getRichMarkdownCommentBlocks(editor)
const selectedText = comment.selectedText?.trim()
if (!selectedText) {
return []
@@ -173,7 +121,7 @@ export function getRichMarkdownCommentAtPos(
if (comments.length === 0) {
return null
}
const blocks = buildRichMarkdownCommentBlocks(editor)
const blocks = getRichMarkdownCommentBlocks(editor)
return (
comments.find((comment) =>
getRichMarkdownAnnotationHighlightRangesForComment(
@@ -216,7 +164,7 @@ export function getRichMarkdownCommentAnchorTop(
}
function getRichMarkdownSelectionRange(editor: Editor): RichMarkdownComposerState {
const blocks = buildRichMarkdownCommentBlocks(editor)
const blocks = getRichMarkdownCommentBlocks(editor)
const { from, to, empty } = editor.state.selection
const selectedBlocks = empty
? blocks.filter((block) => block.from <= from && from <= block.to)
@@ -1,7 +1,7 @@
import type { Editor } from '@tiptap/react'
import type { DiffComment } from '../../../../shared/diff-comment-types'
import { getRichMarkdownCommentAnchorTop } from './rich-markdown-review-annotations'
import { getRichMarkdownReviewRailBlocks } from './rich-markdown-review-rail-blocks'
import { getRichMarkdownCommentBlocks } from './rich-markdown-comment-blocks'
import {
stackRichMarkdownReviewNotePositions,
type RichMarkdownReviewNotePosition
@@ -21,7 +21,7 @@ export function measureRichMarkdownReviewNotePositions({
markdownSourceLineOffset
}: MeasureRichMarkdownReviewNotePositionsOptions): RichMarkdownReviewNotePosition[] {
const containerRect = container.getBoundingClientRect()
const blocks = getRichMarkdownReviewRailBlocks(editor)
const blocks = getRichMarkdownCommentBlocks(editor)
const nextPositions = markdownComments
.map((comment): RichMarkdownReviewNotePosition | null => {
const bodyLineNumber = Math.max(1, comment.lineNumber - markdownSourceLineOffset)
@@ -48,7 +48,8 @@ function measureBaseline({
block,
containerRect,
container.scrollTop,
markdownSourceLineOffset
markdownSourceLineOffset,
buildRichMarkdownCommentBlocks(editor)
)
return top === null ? null : { comment, top }
})
@@ -1,31 +1 @@
import type { Editor } from '@tiptap/core'
import {
buildRichMarkdownCommentBlocks,
type RichMarkdownCommentBlock
} from './rich-markdown-review-annotations'
type ReviewRailBlocks = {
doc: Editor['state']['doc']
markdown: Editor['markdown']
serialize: NonNullable<Editor['markdown']>['serialize'] | undefined
blocks: readonly RichMarkdownCommentBlock[]
}
// Keep only the current document per editor; scrolling changes geometry, not source lines.
const blocksByEditor = new WeakMap<Editor, ReviewRailBlocks>()
export function getRichMarkdownReviewRailBlocks(
editor: Editor
): readonly RichMarkdownCommentBlock[] {
const doc = editor.state.doc
const markdown = editor.markdown
const serialize = markdown?.serialize
const cached = blocksByEditor.get(editor)
if (cached?.doc === doc && cached.markdown === markdown && cached.serialize === serialize) {
return cached.blocks
}
const blocks = buildRichMarkdownCommentBlocks(editor)
blocksByEditor.set(editor, { doc, markdown, serialize, blocks })
return blocks
}
export { getRichMarkdownCommentBlocks as getRichMarkdownReviewRailBlocks } from './rich-markdown-comment-blocks'
@@ -0,0 +1,209 @@
import type { Locator, Page } from '@stablyai/playwright-test'
import type { Editor, JSONContent } from '@tiptap/core'
import type { MarkdownManager } from '@tiptap/markdown'
import { expect, test } from './helpers/orca-app'
import {
cleanupMarkdownFixture,
createMarkdownFixture,
getActiveWorktreeContext,
openMarkdownFixture,
waitForRichMarkdownEditor
} from './helpers/markdown-editor-fixture'
const PARAGRAPH_COUNT = Number(process.env.ORCA_MARKDOWN_REVIEW_PERF_PARAGRAPHS ?? '500')
const targetIndex = Math.floor(PARAGRAPH_COUNT / 2)
const paragraph = (index: number) =>
`Paragraph ${index}. Ordinary editing and selection of plain prose.`
const SOURCE = Array.from({ length: PARAGRAPH_COUNT }, (_, index) => paragraph(index)).join('\n\n')
type ReviewSelectionMetrics = {
calls: number
jsonCalls: number
serializeMs: number
jsonMs: number
frameGaps: number[]
}
type ReviewSelectionProbe = {
snapshot: () => ReviewSelectionMetrics
reset: () => void
restore: () => void
}
type PageRichMarkdownReviewEditorElement = HTMLElement & {
editor?: Editor & { markdown?: MarkdownManager }
__reviewSelectionProbe?: ReviewSelectionProbe
}
async function frames(page: Page) {
await page.evaluate(
() =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
)
)
}
async function measure(editor: Locator, reset = false): Promise<ReviewSelectionMetrics> {
return editor.evaluate((element, reset) => {
const editorElement =
element.closest<PageRichMarkdownReviewEditorElement>('.rich-markdown-editor')
const probe = editorElement?.__reviewSelectionProbe
if (!probe) {
throw new Error('Review selection probe missing')
}
if (reset) {
probe.reset()
}
return probe.snapshot()
}, reset)
}
test('selection and scrolling reuse Markdown review source lines', async ({
orcaPage,
registerPostElectronShutdownCleanup
}, testInfo) => {
test.setTimeout(180_000)
expect(Buffer.byteLength(SOURCE)).toBeLessThan(600 * 1024)
const context = await getActiveWorktreeContext(orcaPage)
const filePath = await createMarkdownFixture(
context,
'.orca-e2e-markdown-review-perf',
'review-selection',
testInfo.workerIndex,
SOURCE
)
registerPostElectronShutdownCleanup(() => cleanupMarkdownFixture(filePath))
await openMarkdownFixture(orcaPage, context, filePath)
const editor = await waitForRichMarkdownEditor(orcaPage)
await expect(editor.locator('p')).toHaveCount(PARAGRAPH_COUNT, { timeout: 60_000 })
await editor.evaluate((element) => {
const editorElement =
element.closest<PageRichMarkdownReviewEditorElement>('.rich-markdown-editor')
const instance = editorElement?.editor
if (!editorElement || !instance) {
throw new Error('Editor unavailable')
}
const markdown = instance.markdown
if (!markdown) {
throw new Error('Markdown manager missing')
}
const serialize = markdown.serialize
const getJSON = instance.getJSON
let calls = 0
let jsonCalls = 0
let serializeMs = 0
let jsonMs = 0
let frameGaps: number[] = []
let previousFrame: number | null = null
let frameId = 0
const tick = (now: number) => {
if (previousFrame !== null) {
frameGaps.push(now - previousFrame)
}
previousFrame = now
frameId = requestAnimationFrame(tick)
}
frameId = requestAnimationFrame(tick)
markdown.serialize = (content: JSONContent): string => {
const start = performance.now()
try {
calls++
return serialize.call(markdown, content)
} finally {
serializeMs += performance.now() - start
}
}
instance.getJSON = (): ReturnType<Editor['getJSON']> => {
const start = performance.now()
try {
jsonCalls++
return getJSON.call(instance)
} finally {
jsonMs += performance.now() - start
}
}
editorElement.__reviewSelectionProbe = {
snapshot: () => ({ calls, jsonCalls, serializeMs, jsonMs, frameGaps }),
reset: () => {
calls = 0
jsonCalls = 0
serializeMs = 0
jsonMs = 0
frameGaps = []
previousFrame = null
},
restore: () => {
cancelAnimationFrame(frameId)
markdown.serialize = serialize
instance.getJSON = getJSON
}
}
})
try {
const target = editor.getByText(paragraph(targetIndex), { exact: true })
await target.evaluate((element) => element.scrollIntoView({ block: 'center' }))
const point = await target.evaluate((element) => {
const text = element.firstChild
if (!(text instanceof Text)) {
throw new Error('Paragraph text missing')
}
const range = document.createRange()
range.setStart(text, 0)
range.collapse(true)
const rect = range.getBoundingClientRect()
return { x: rect.left, y: rect.top + rect.height / 2 }
})
await orcaPage.mouse.click(point.x, point.y)
await orcaPage.keyboard.press('Shift+ArrowRight')
await expect(
orcaPage.getByRole('button', { name: 'Add review note', exact: true })
).toBeVisible()
await frames(orcaPage)
const cold = await measure(editor)
await measure(editor, true)
for (let index = 0; index < 20; index++) {
await orcaPage.keyboard.press('Shift+ArrowRight')
await frames(orcaPage)
}
const selection = await measure(editor)
expect(await orcaPage.evaluate(() => window.getSelection()?.toString())).toBe(
paragraph(targetIndex).slice(0, 21)
)
const viewport = orcaPage.locator('.rich-markdown-editor-shell .overflow-auto')
await measure(editor, true)
for (let index = 0; index < 10; index++) {
await viewport.evaluate((element, index) => {
element.scrollTop += index % 2 ? -8 : 8
}, index)
await frames(orcaPage)
}
const scroll = await measure(editor)
const result = {
paragraphs: PARAGRAPH_COUNT,
sourceBytes: Buffer.byteLength(SOURCE),
cold,
selection,
scroll
}
await testInfo.attach('review-selection-metrics', {
body: JSON.stringify(result, null, 2),
contentType: 'application/json'
})
process.stdout.write(`${JSON.stringify(result)}\n`)
await expect(editor).toBeFocused()
const tab = orcaPage.locator('[data-tab-id]').filter({ hasText: 'review-selection' }).last()
await expect(tab.locator('span.rounded-full')).toHaveCount(0)
await orcaPage.screenshot({ path: testInfo.outputPath('review-selection.png') })
expect(selection.calls).toBe(0)
expect(selection.jsonCalls).toBe(0)
expect(scroll.calls).toBe(0)
expect(scroll.jsonCalls).toBe(0)
} finally {
await editor.evaluate((element) => {
const editorElement =
element.closest<PageRichMarkdownReviewEditorElement>('.rich-markdown-editor')
editorElement?.__reviewSelectionProbe?.restore()
})
}
})