diff --git a/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx b/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx index 12bc5cc35c3..8e2ca1e4abb 100644 --- a/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx +++ b/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx @@ -206,7 +206,8 @@ export function RichMarkdownEditorSurface({
{ if (!shouldFocusEmptyEditorFromSurfaceClick(event, editor)) { return diff --git a/tests/e2e/markdown-tab-scroll-restore.spec.ts b/tests/e2e/markdown-tab-scroll-restore.spec.ts new file mode 100644 index 00000000000..02362dd822a --- /dev/null +++ b/tests/e2e/markdown-tab-scroll-restore.spec.ts @@ -0,0 +1,103 @@ +import { writeFile, rm } from 'node:fs/promises' +import path from 'node:path' +import { test, expect } from './helpers/orca-app' +import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { + cleanupMarkdownFixture, + createMarkdownFixture, + getActiveWorktreeContext, + openMarkdownFixture, + waitForRichMarkdownEditor +} from './helpers/markdown-editor-fixture' + +test('restores the Markdown viewport when an image gains height after a tab switch', async ({ + orcaPage +}, testInfo) => { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + const context = await getActiveWorktreeContext(orcaPage) + const directory = '.orca-e2e-markdown-scroll' + let filePath: string | null = null + let otherPath: string | null = null + let imagePath: string | null = null + + try { + const sections = Array.from( + { length: 100 }, + (_, index) => `## Section ${index}\n\nParagraph ${index}. Scroll restoration testing text.` + ).join('\n\n') + filePath = await createMarkdownFixture( + context, + directory, + 'image-scroll', + testInfo.workerIndex, + `# Image scroll\n\n![Scroll restoration image](tall.svg)\n\n${sections}` + ) + imagePath = path.join(path.dirname(filePath), 'tall.svg') + await writeFile( + imagePath, + '' + ) + otherPath = await createMarkdownFixture( + context, + directory, + 'other-tab', + testInfo.workerIndex, + '# Other tab' + ) + await openMarkdownFixture(orcaPage, context, otherPath) + await waitForRichMarkdownEditor(orcaPage) + await openMarkdownFixture(orcaPage, context, filePath) + const editor = await waitForRichMarkdownEditor(orcaPage) + const image = editor.getByRole('img', { name: 'Scroll restoration image' }) + await expect + .poll(() => + image.evaluate((element) => + element instanceof HTMLImageElement ? element.naturalHeight : 0 + ) + ) + .toBe(1500) + const viewport = orcaPage.locator('.rich-markdown-editor-shell .overflow-auto') + await viewport.evaluate((element) => { + element.scrollTop = 4000 + }) + const heading = editor.getByRole('heading', { name: 'Section 45', exact: true }) + const originalTop = await heading.evaluate((element) => element.getBoundingClientRect().top) + + await orcaPage + .locator('[data-tab-id]') + .filter({ hasText: path.basename(otherPath) }) + .click() + // Model image dimensions arriving after restoration, independent of the host's decode speed. + const pendingImage = await orcaPage.addStyleTag({ + content: + '.rich-markdown-editor img[alt="Scroll restoration image"] { height: 1px !important; }' + }) + await orcaPage + .locator('[data-tab-id]') + .filter({ hasText: path.basename(filePath) }) + .click() + await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(4000) + await pendingImage.evaluate((element) => element.remove()) + await expect + .poll(() => image.evaluate((element) => element.getBoundingClientRect().height)) + .toBeGreaterThan(500) + // Let Chromium apply its scroll-anchor adjustment before checking the final viewport. + await viewport.evaluate( + () => + new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + }) + ) + await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(4000) + await expect + .poll(() => heading.evaluate((element) => element.getBoundingClientRect().top)) + .toBe(originalTop) + } finally { + await cleanupMarkdownFixture(filePath) + await cleanupMarkdownFixture(otherPath) + if (imagePath) { + await rm(imagePath, { force: true }) + } + } +})