fix(ai): remount composer after native input
This commit is contained in:
@@ -779,8 +779,6 @@ describe('App', () => {
|
||||
fireEvent.keyDown(window, { key: 'l', code: 'KeyL', metaKey: true, shiftKey: true })
|
||||
|
||||
const input = await screen.findByTestId('agent-input')
|
||||
input.textContent = 'Summarize the active vault'
|
||||
fireEvent.input(input)
|
||||
fireEvent.click(screen.getByTestId('agent-send'))
|
||||
|
||||
await act(async () => {
|
||||
@@ -805,6 +803,8 @@ describe('App', () => {
|
||||
expect(input).toHaveAttribute('aria-placeholder', 'Ask Codex')
|
||||
})
|
||||
|
||||
input.textContent = 'Summarize the active vault'
|
||||
fireEvent.input(input)
|
||||
fireEvent.click(screen.getByTestId('agent-send'))
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -114,7 +114,7 @@ function updateAiInput(text: string) {
|
||||
editor.textContent = text
|
||||
setSelection(editor, text.length)
|
||||
fireEvent.input(editor)
|
||||
return editor
|
||||
return screen.queryByTestId('command-palette-ai-input') ?? editor
|
||||
}
|
||||
|
||||
function resetNativeDropState() {
|
||||
|
||||
@@ -191,7 +191,6 @@ export function InlineWikilinkInput({
|
||||
setSelectionRange,
|
||||
setCombinedRef,
|
||||
syncSelectionRange,
|
||||
commitValueFromEditor,
|
||||
focusSelectionRange,
|
||||
} = useInlineWikilinkSelection({
|
||||
value,
|
||||
@@ -396,7 +395,20 @@ export function InlineWikilinkInput({
|
||||
}
|
||||
}
|
||||
|
||||
commitValueFromEditor()
|
||||
if (!editor) return
|
||||
|
||||
const nextValue = normalizeInlineWikilinkValue(serializeInlineNode(editor))
|
||||
const nextSelection = readSelectionRange(editor)
|
||||
const clampedSelection: InlineSelectionRange = {
|
||||
start: Math.min(nextSelection.start, nextValue.length),
|
||||
end: Math.min(nextSelection.end, nextValue.length),
|
||||
}
|
||||
|
||||
const shouldRestoreFocus = document.activeElement === editor
|
||||
pendingFocusAfterRemountRef.current = shouldRestoreFocus ? clampedSelection : null
|
||||
onChange(nextValue)
|
||||
setSelectionRange(clampedSelection)
|
||||
forceRender((current) => current + 1)
|
||||
}
|
||||
const flushPendingCompositionInput = () => {
|
||||
if (isComposingRef.current || !pendingCompositionInputRef.current) return
|
||||
@@ -431,6 +443,8 @@ export function InlineWikilinkInput({
|
||||
queueMicrotask(flushPendingCompositionInput)
|
||||
}
|
||||
const handleInput = () => {
|
||||
if (disabled) return
|
||||
|
||||
if (isComposingRef.current) {
|
||||
pendingCompositionInputRef.current = true
|
||||
return
|
||||
|
||||
@@ -416,16 +416,37 @@ describe('WikilinkChatInput', () => {
|
||||
fireEvent.input(editor)
|
||||
expect(onDraftChange).toHaveBeenLastCalledWith(portugueseText)
|
||||
|
||||
const cjkKey = createEvent.keyDown(editor, { key: '你' })
|
||||
fireEvent(editor, cjkKey)
|
||||
const updatedEditor = screen.getByTestId('agent-input')
|
||||
const cjkKey = createEvent.keyDown(updatedEditor, { key: '你' })
|
||||
fireEvent(updatedEditor, cjkKey)
|
||||
expect(cjkKey.defaultPrevented).toBe(false)
|
||||
|
||||
editor.textContent = `${portugueseText}你`
|
||||
setSelection(editor, portugueseText.length + 1)
|
||||
fireEvent.input(editor)
|
||||
updatedEditor.textContent = `${portugueseText}你`
|
||||
setSelection(updatedEditor, portugueseText.length + 1)
|
||||
fireEvent.input(updatedEditor)
|
||||
expect(onDraftChange).toHaveBeenLastCalledWith(`${portugueseText}你`)
|
||||
})
|
||||
|
||||
it('remounts after native DOM input so React does not diff a mutated contentEditable tree', async () => {
|
||||
const onDraftChange = vi.fn()
|
||||
render(<Controlled onDraftChange={onDraftChange} />)
|
||||
const initialEditor = screen.getByTestId('agent-input') as HTMLDivElement
|
||||
initialEditor.focus()
|
||||
|
||||
initialEditor.textContent = 'follow up after edit'
|
||||
setSelection(initialEditor, 'follow up after edit'.length)
|
||||
fireEvent.input(initialEditor)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onDraftChange).toHaveBeenLastCalledWith('follow up after edit')
|
||||
})
|
||||
|
||||
const remountedEditor = screen.getByTestId('agent-input') as HTMLDivElement
|
||||
expect(remountedEditor).not.toBe(initialEditor)
|
||||
expect(remountedEditor.textContent).toBe('follow up after edit')
|
||||
expect(document.activeElement).toBe(remountedEditor)
|
||||
})
|
||||
|
||||
it('deletes an inline chip with a single Backspace', () => {
|
||||
render(<Controlled />)
|
||||
updateEditorText('edit my [[alp')
|
||||
|
||||
@@ -35,6 +35,24 @@ test.describe('Inline wikilink editor regression', () => {
|
||||
await expectNoPageErrors(pageErrors)
|
||||
})
|
||||
|
||||
test('keeps follow-up typing stable after editing the active note body', async ({ page }) => {
|
||||
const pageErrors = trackPageErrors(page)
|
||||
const noteBlock = page.locator('.bn-block-content').nth(1)
|
||||
const editor = page.getByTestId('agent-input')
|
||||
const noteMarker = ` follow-up guard ${Date.now()}`
|
||||
|
||||
await expect(noteBlock).toBeVisible({ timeout: 5_000 })
|
||||
await noteBlock.click()
|
||||
await page.keyboard.type(noteMarker)
|
||||
await expect(page.locator('.bn-editor')).toContainText(noteMarker.trim())
|
||||
|
||||
await editor.click()
|
||||
await page.keyboard.type('follow up after note edit')
|
||||
|
||||
await expectNormalizedEditorText(editor, 'follow up after note edit')
|
||||
await expectNoPageErrors(pageErrors)
|
||||
})
|
||||
|
||||
test('keeps select-all cut scoped to the AI panel chat input', async ({ page }) => {
|
||||
const pageErrors = trackPageErrors(page)
|
||||
const editor = page.getByTestId('agent-input')
|
||||
|
||||
Reference in New Issue
Block a user