mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
Merge branch 'nwparker/markdown-fallback-preview' into stack-preview
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html'
|
||||
import { createRichMarkdownExtensions } from './rich-markdown-extensions'
|
||||
import { commitRichMarkdownSerialization } from './rich-markdown-serialization-commit'
|
||||
@@ -144,3 +144,47 @@ for (const eol of ['\n', '\r\n', '\r']) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const eol of ['\n', '\r\n', '\r']) {
|
||||
it.each(['null', 'throw'])(
|
||||
`recovers after a %s reconciliation failure with EOL ${JSON.stringify(eol)}`,
|
||||
(failure) => {
|
||||
const source = '# target\n\n_unchanged_\n'.replace(/\n/g, eol)
|
||||
const editor = openDocument(source)
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
const refs = {
|
||||
originalSourceRef: { current: source },
|
||||
baseCanonicalRef: { current: editor.getMarkdown() },
|
||||
lastCommittedMarkdownRef: { current: source }
|
||||
}
|
||||
editor.view.dispatch(editor.state.tr.insertText('changed', 1, 7))
|
||||
const saved = commitRichMarkdownSerialization(editor, refs, () => {
|
||||
if (failure === 'throw') {
|
||||
throw new Error('Injected round-trip failure')
|
||||
}
|
||||
return null
|
||||
})
|
||||
expect(canonicalize(saved.markdown)).toBe(editor.getMarkdown())
|
||||
expect(saved.markdown.endsWith(eol)).toBe(true)
|
||||
expect(refs.lastCommittedMarkdownRef.current).toBe(saved.markdown)
|
||||
const serialize = vi.spyOn(editor, 'getMarkdown').mockImplementationOnce(() => {
|
||||
throw new Error('Injected editor teardown')
|
||||
})
|
||||
expect(commitRichMarkdownSerialization(editor, refs, canonicalize)).toEqual({
|
||||
markdown: saved.markdown,
|
||||
didSerialize: false
|
||||
})
|
||||
serialize.mockRestore()
|
||||
editor.view.dispatch(editor.state.tr.insertText('recovered', 1, 8))
|
||||
const recovered = commitRichMarkdownSerialization(editor, refs, canonicalize)
|
||||
expect(recovered.didSerialize).toBe(true)
|
||||
expect(canonicalize(recovered.markdown)).toBe(editor.getMarkdown())
|
||||
expect(recovered.markdown).toContain('recovered')
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
editor.destroy()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,25 +133,29 @@ function stripTrailingNewlines(lfText: string): string {
|
||||
return lfText.replace(/\n+$/, '')
|
||||
}
|
||||
|
||||
function detectDominantEol(text: string): '\n' | '\r\n' {
|
||||
function detectDominantEol(text: string): '\n' | '\r\n' | '\r' {
|
||||
const totalLf = (text.match(/\n/g) ?? []).length
|
||||
const crlf = (text.match(/\r\n/g) ?? []).length
|
||||
const lfOnly = totalLf - crlf
|
||||
const crOnly = (text.match(/\r/g) ?? []).length - crlf
|
||||
if (crOnly > Math.max(lfOnly, crlf)) {
|
||||
return '\r'
|
||||
}
|
||||
return crlf > 0 && crlf >= lfOnly ? '\r\n' : '\n'
|
||||
}
|
||||
|
||||
function toLf(text: string): string {
|
||||
return text.replace(/\r\n/g, '\n')
|
||||
return text.replace(/\r\n|\r/g, '\n')
|
||||
}
|
||||
|
||||
function restoreEol(lfText: string, eol: '\n' | '\r\n'): string {
|
||||
function restoreEol(lfText: string, eol: '\n' | '\r\n' | '\r'): string {
|
||||
// lfText is pure LF, so a blind LF→CRLF replace produces no mixed endings.
|
||||
return eol === '\r\n' ? lfText.replace(/\n/g, '\r\n') : lfText
|
||||
return eol === '\n' ? lfText : lfText.replace(/\n/g, eol)
|
||||
}
|
||||
|
||||
function normalizeForSafety(text: string): string {
|
||||
// Why: compare exactly (only CRLF-normalized) — a trailing `\n\n` empty paragraph is semantic, so a lenient trimEnd would mask the trailing-block drift branch 6 must catch.
|
||||
return text.replace(/\r\n/g, '\n')
|
||||
return text.replace(/\r\n|\r/g, '\n')
|
||||
}
|
||||
|
||||
function getUtf8OffsetsAtCodeUnitIndices(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import {
|
||||
cleanupMarkdownFixture,
|
||||
closeActiveEditorTab,
|
||||
createMarkdownFixture,
|
||||
getActiveWorktreeContext,
|
||||
openMarkdownFixture,
|
||||
waitForRichMarkdownEditor
|
||||
} from './helpers/markdown-editor-fixture'
|
||||
|
||||
const LABEL = '[label](path) $5'
|
||||
|
||||
test('literal link labels survive editing and reopening', async ({ orcaPage }, testInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
const context = await getActiveWorktreeContext(orcaPage)
|
||||
const file = await createMarkdownFixture(
|
||||
context,
|
||||
'markdown-links',
|
||||
'literal-label',
|
||||
testInfo.workerIndex,
|
||||
'# Link label safety\n\n[Original label](https://example.com)\n\nUnchanged tail.\n'
|
||||
)
|
||||
try {
|
||||
await openMarkdownFixture(orcaPage, context, file)
|
||||
const editor = await waitForRichMarkdownEditor(orcaPage)
|
||||
await expect(editor.locator('a')).toHaveText('Original label')
|
||||
await testInfo.attach('before-edit', {
|
||||
body: await orcaPage.screenshot({ path: testInfo.outputPath('before-edit.png') }),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
await editor.locator('a').evaluate((link) => {
|
||||
link.closest<HTMLElement>('[contenteditable]')?.focus()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(link)
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
document.dispatchEvent(new Event('selectionchange'))
|
||||
})
|
||||
await expect
|
||||
.poll(() => orcaPage.evaluate(() => window.getSelection()?.toString()))
|
||||
.toBe('Original label')
|
||||
await orcaPage.keyboard.insertText(LABEL)
|
||||
await expect(editor.locator('a')).toHaveText(LABEL)
|
||||
await orcaPage.keyboard.press('ControlOrMeta+S')
|
||||
await expect.poll(() => readFileSync(file, 'utf8')).not.toContain('Original label')
|
||||
expect(readFileSync(file, 'utf8')).toContain('https://example.com')
|
||||
await closeActiveEditorTab(orcaPage, file)
|
||||
await openMarkdownFixture(orcaPage, context, file)
|
||||
const reopened = await waitForRichMarkdownEditor(orcaPage)
|
||||
await expect(reopened.locator('a')).toHaveCount(1)
|
||||
await expect(reopened.locator('a')).toHaveText(LABEL)
|
||||
await expect(reopened.locator('a')).toHaveAttribute('href', 'https://example.com')
|
||||
await expect(reopened.locator('p').last()).toHaveText('Unchanged tail.')
|
||||
await testInfo.attach('after-reopen', {
|
||||
body: await orcaPage.screenshot({ path: testInfo.outputPath('after-reopen.png') }),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
} finally {
|
||||
await cleanupMarkdownFixture(file)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user