mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 18:06:36 +08:00
192 lines
6.0 KiB
TypeScript
192 lines
6.0 KiB
TypeScript
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
|
|
import type { ComponentProps } from 'react'
|
|
import type { EventEmitterValue } from '@/context/event-emitter'
|
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { getNearestEditorFromDOMNode } from 'lexical'
|
|
import { useEffect } from 'react'
|
|
import PromptEditor from '@/app/components/base/prompt-editor'
|
|
import {
|
|
UPDATE_DATASETS_EVENT_EMITTER,
|
|
UPDATE_HISTORY_EVENT_EMITTER,
|
|
} from '@/app/components/base/prompt-editor/constants'
|
|
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block'
|
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
|
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
|
|
|
type Captures = {
|
|
eventEmitter: EventEmitter<EventEmitterValue> | null
|
|
events: EventEmitterValue[]
|
|
}
|
|
|
|
const EventProbe = ({ captures }: { captures: Captures }) => {
|
|
const { eventEmitter } = useEventEmitterContextContext()
|
|
|
|
useEffect(() => {
|
|
captures.eventEmitter = eventEmitter
|
|
}, [captures, eventEmitter])
|
|
|
|
eventEmitter?.useSubscription((value) => {
|
|
captures.events.push(value)
|
|
})
|
|
|
|
return <button type="button">outside</button>
|
|
}
|
|
|
|
const PromptEditorHarness = ({
|
|
captures,
|
|
...props
|
|
}: ComponentProps<typeof PromptEditor> & { captures: Captures }) => (
|
|
<EventEmitterContextProvider>
|
|
<EventProbe captures={captures} />
|
|
<PromptEditor {...props} />
|
|
</EventEmitterContextProvider>
|
|
)
|
|
|
|
describe('Base Prompt Editor Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// Real prompt editor integration should emit block updates and transform editor updates into text output.
|
|
describe('Editor Shell', () => {
|
|
it('should render with the real editor, emit dataset/history events, and convert update events into text changes', async () => {
|
|
const captures: Captures = { eventEmitter: null, events: [] }
|
|
const onChange = vi.fn()
|
|
const onFocus = vi.fn()
|
|
const onBlur = vi.fn()
|
|
const user = userEvent.setup()
|
|
|
|
const { rerender, container } = render(
|
|
<PromptEditorHarness
|
|
captures={captures}
|
|
instanceId="editor-1"
|
|
compact={true}
|
|
className="editor-shell"
|
|
placeholder="Type prompt"
|
|
onChange={onChange}
|
|
onFocus={onFocus}
|
|
onBlur={onBlur}
|
|
contextBlock={{
|
|
show: false,
|
|
datasets: [{ id: 'ds-1', name: 'Dataset One', type: 'dataset' }],
|
|
}}
|
|
historyBlock={{
|
|
show: false,
|
|
history: { user: 'user-role', assistant: 'assistant-role' },
|
|
}}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('Type prompt')).toBeInTheDocument()
|
|
|
|
await waitFor(() => {
|
|
expect(captures.eventEmitter).not.toBeNull()
|
|
})
|
|
|
|
const editable = container.querySelector('[contenteditable="true"]') as HTMLElement
|
|
expect(editable).toBeInTheDocument()
|
|
|
|
await user.click(editable)
|
|
await waitFor(() => {
|
|
expect(onFocus).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
await user.click(screen.getByRole('button', { name: 'outside' }))
|
|
await waitFor(() => {
|
|
expect(onBlur).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
act(() => {
|
|
captures.eventEmitter?.emit({
|
|
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
|
|
instanceId: 'editor-1',
|
|
payload: 'first line\nsecond line',
|
|
})
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(onChange).toHaveBeenCalledWith('first line\nsecond line')
|
|
})
|
|
|
|
expect(captures.events).toContainEqual({
|
|
type: UPDATE_DATASETS_EVENT_EMITTER,
|
|
payload: [{ id: 'ds-1', name: 'Dataset One', type: 'dataset' }],
|
|
})
|
|
expect(captures.events).toContainEqual({
|
|
type: UPDATE_HISTORY_EVENT_EMITTER,
|
|
payload: { user: 'user-role', assistant: 'assistant-role' },
|
|
})
|
|
|
|
rerender(
|
|
<PromptEditorHarness
|
|
captures={captures}
|
|
instanceId="editor-1"
|
|
contextBlock={{
|
|
show: false,
|
|
datasets: [{ id: 'ds-2', name: 'Dataset Two', type: 'dataset' }],
|
|
}}
|
|
historyBlock={{
|
|
show: false,
|
|
history: { user: 'user-next', assistant: 'assistant-next' },
|
|
}}
|
|
/>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(captures.events).toContainEqual({
|
|
type: UPDATE_DATASETS_EVENT_EMITTER,
|
|
payload: [{ id: 'ds-2', name: 'Dataset Two', type: 'dataset' }],
|
|
})
|
|
})
|
|
expect(captures.events).toContainEqual({
|
|
type: UPDATE_HISTORY_EVENT_EMITTER,
|
|
payload: { user: 'user-next', assistant: 'assistant-next' },
|
|
})
|
|
})
|
|
|
|
it('should tolerate updates without onChange and rethrow lexical runtime errors through the configured handler', async () => {
|
|
const captures: Captures = { eventEmitter: null, events: [] }
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const { container } = render(
|
|
<PromptEditorHarness
|
|
captures={captures}
|
|
instanceId="editor-2"
|
|
editable={false}
|
|
placeholder="Read only prompt"
|
|
/>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(captures.eventEmitter).not.toBeNull()
|
|
})
|
|
|
|
act(() => {
|
|
captures.eventEmitter?.emit({
|
|
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
|
|
instanceId: 'editor-2',
|
|
payload: 'silent update',
|
|
})
|
|
})
|
|
|
|
const editable = container.querySelector('[contenteditable="false"]') as HTMLElement
|
|
const editor = getNearestEditorFromDOMNode(editable)
|
|
|
|
expect(editable).toBeInTheDocument()
|
|
expect(editor).not.toBeNull()
|
|
expect(screen.getByRole('textbox')).toHaveTextContent('silent update')
|
|
|
|
expect(() => {
|
|
act(() => {
|
|
editor?.update(() => {
|
|
throw new Error('prompt-editor boom')
|
|
})
|
|
})
|
|
}).toThrow('prompt-editor boom')
|
|
|
|
consoleErrorSpy.mockRestore()
|
|
})
|
|
})
|
|
})
|