mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test: add unit tests for base components-part-4 (#32452)
Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,95 @@
|
||||
import type { LexicalComposerContextWithEditor } from '@lexical/react/LexicalComposerContext'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { ReactElement } from 'react'
|
||||
import { LexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import ErrorMessageBlockComponent from './component'
|
||||
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from './index'
|
||||
|
||||
vi.mock('../../hooks')
|
||||
|
||||
const mockHasNodes = vi.fn()
|
||||
|
||||
const mockEditor = {
|
||||
hasNodes: mockHasNodes,
|
||||
} as unknown as LexicalEditor
|
||||
|
||||
const lexicalContextValue: LexicalComposerContextWithEditor = [
|
||||
mockEditor,
|
||||
{ getTheme: () => undefined },
|
||||
]
|
||||
|
||||
const renderWithLexicalContext = (ui: ReactElement) => {
|
||||
return render(
|
||||
<LexicalComposerContext.Provider value={lexicalContextValue}>
|
||||
{ui}
|
||||
</LexicalComposerContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ErrorMessageBlockComponent', () => {
|
||||
const mockRef = { current: null as HTMLDivElement | null }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHasNodes.mockReturnValue(true)
|
||||
vi.mocked(useSelectOrDelete).mockReturnValue([mockRef, false])
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render error_message text and base styles when unselected', () => {
|
||||
const { container } = renderWithLexicalContext(<ErrorMessageBlockComponent nodeKey="node-1" />)
|
||||
|
||||
expect(screen.getByText('error_message')).toBeInTheDocument()
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
expect(container.firstChild).toHaveClass('border-components-panel-border-subtle')
|
||||
})
|
||||
|
||||
it('should render selected styles when node is selected', () => {
|
||||
vi.mocked(useSelectOrDelete).mockReturnValue([mockRef, true])
|
||||
|
||||
const { container } = renderWithLexicalContext(<ErrorMessageBlockComponent nodeKey="node-1" />)
|
||||
|
||||
expect(container.firstChild).toHaveClass('border-state-accent-solid')
|
||||
expect(container.firstChild).toHaveClass('bg-state-accent-hover')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should stop propagation when wrapper is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onParentClick = vi.fn()
|
||||
|
||||
render(
|
||||
<LexicalComposerContext.Provider value={lexicalContextValue}>
|
||||
<div onClick={onParentClick}>
|
||||
<ErrorMessageBlockComponent nodeKey="node-1" />
|
||||
</div>
|
||||
</LexicalComposerContext.Provider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('error_message'))
|
||||
|
||||
expect(onParentClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hooks', () => {
|
||||
it('should use selection hook and check node registration on mount', () => {
|
||||
renderWithLexicalContext(<ErrorMessageBlockComponent nodeKey="node-xyz" />)
|
||||
|
||||
expect(useSelectOrDelete).toHaveBeenCalledWith('node-xyz', DELETE_ERROR_MESSAGE_COMMAND)
|
||||
expect(mockHasNodes).toHaveBeenCalledWith([ErrorMessageBlockNode])
|
||||
})
|
||||
|
||||
it('should throw when ErrorMessageBlockNode is not registered', () => {
|
||||
mockHasNodes.mockReturnValue(false)
|
||||
|
||||
expect(() => renderWithLexicalContext(<ErrorMessageBlockComponent nodeKey="node-1" />)).toThrow(
|
||||
'WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,125 @@
|
||||
import type { LexicalComposerContextWithEditor } from '@lexical/react/LexicalComposerContext'
|
||||
import type { EntityMatch } from '@lexical/text'
|
||||
import type { LexicalEditor, LexicalNode } from 'lexical'
|
||||
import type { ReactElement } from 'react'
|
||||
import { LexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { render } from '@testing-library/react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import ErrorMessageBlockReplacementBlock from './error-message-block-replacement-block'
|
||||
import { $createErrorMessageBlockNode, ErrorMessageBlockNode } from './node'
|
||||
|
||||
vi.mock('@lexical/utils')
|
||||
vi.mock('lexical')
|
||||
vi.mock('../../utils')
|
||||
vi.mock('./node')
|
||||
|
||||
const mockHasNodes = vi.fn()
|
||||
const mockRegisterNodeTransform = vi.fn()
|
||||
|
||||
const mockEditor = {
|
||||
hasNodes: mockHasNodes,
|
||||
registerNodeTransform: mockRegisterNodeTransform,
|
||||
} as unknown as LexicalEditor
|
||||
|
||||
const lexicalContextValue: LexicalComposerContextWithEditor = [
|
||||
mockEditor,
|
||||
{ getTheme: () => undefined },
|
||||
]
|
||||
|
||||
const renderWithLexicalContext = (ui: ReactElement) => {
|
||||
return render(
|
||||
<LexicalComposerContext.Provider value={lexicalContextValue}>
|
||||
{ui}
|
||||
</LexicalComposerContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ErrorMessageBlockReplacementBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHasNodes.mockReturnValue(true)
|
||||
mockRegisterNodeTransform.mockReturnValue(vi.fn())
|
||||
vi.mocked(mergeRegister).mockImplementation((...cleanups) => {
|
||||
return () => cleanups.forEach(cleanup => cleanup())
|
||||
})
|
||||
vi.mocked($createErrorMessageBlockNode).mockReturnValue({ type: 'node' } as unknown as ErrorMessageBlockNode)
|
||||
vi.mocked($applyNodeReplacement).mockImplementation((node: LexicalNode) => node)
|
||||
})
|
||||
|
||||
it('should register transform and cleanup on unmount', () => {
|
||||
const transformCleanup = vi.fn()
|
||||
mockRegisterNodeTransform.mockReturnValue(transformCleanup)
|
||||
|
||||
const { unmount, container } = renderWithLexicalContext(<ErrorMessageBlockReplacementBlock />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
expect(mockHasNodes).toHaveBeenCalledWith([ErrorMessageBlockNode])
|
||||
expect(mockRegisterNodeTransform).toHaveBeenCalledWith(CustomTextNode, expect.any(Function))
|
||||
|
||||
unmount()
|
||||
expect(transformCleanup).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw when ErrorMessageBlockNode is not registered', () => {
|
||||
mockHasNodes.mockReturnValue(false)
|
||||
|
||||
expect(() => renderWithLexicalContext(<ErrorMessageBlockReplacementBlock />)).toThrow(
|
||||
'ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor',
|
||||
)
|
||||
})
|
||||
|
||||
it('should pass matcher and creator to decoratorTransform and match placeholder text', () => {
|
||||
renderWithLexicalContext(<ErrorMessageBlockReplacementBlock />)
|
||||
|
||||
const transformCallback = mockRegisterNodeTransform.mock.calls[0][1] as (node: LexicalNode) => void
|
||||
const textNode = { id: 't-1' } as unknown as LexicalNode
|
||||
transformCallback(textNode)
|
||||
|
||||
expect(decoratorTransform).toHaveBeenCalledWith(
|
||||
textNode,
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
)
|
||||
|
||||
const getMatch = vi.mocked(decoratorTransform).mock.calls[0][1] as (text: string) => EntityMatch | null
|
||||
const match = getMatch(`hello ${ERROR_MESSAGE_PLACEHOLDER_TEXT} world`)
|
||||
|
||||
expect(match).toEqual({
|
||||
start: 6,
|
||||
end: 6 + ERROR_MESSAGE_PLACEHOLDER_TEXT.length,
|
||||
})
|
||||
expect(getMatch('hello world')).toBeNull()
|
||||
})
|
||||
|
||||
it('should create replacement node and call onInsert when creator runs', () => {
|
||||
const onInsert = vi.fn()
|
||||
renderWithLexicalContext(<ErrorMessageBlockReplacementBlock onInsert={onInsert} />)
|
||||
|
||||
const transformCallback = mockRegisterNodeTransform.mock.calls[0][1] as (node: LexicalNode) => void
|
||||
transformCallback({ id: 't-1' } as unknown as LexicalNode)
|
||||
|
||||
const createNode = vi.mocked(decoratorTransform).mock.calls[0][2] as () => ErrorMessageBlockNode
|
||||
const created = createNode()
|
||||
|
||||
expect(onInsert).toHaveBeenCalledTimes(1)
|
||||
expect($createErrorMessageBlockNode).toHaveBeenCalledTimes(1)
|
||||
expect($applyNodeReplacement).toHaveBeenCalledWith({ type: 'node' })
|
||||
expect(created).toEqual({ type: 'node' })
|
||||
})
|
||||
|
||||
it('should create replacement node without onInsert callback', () => {
|
||||
renderWithLexicalContext(<ErrorMessageBlockReplacementBlock />)
|
||||
|
||||
const transformCallback = mockRegisterNodeTransform.mock.calls[0][1] as (node: LexicalNode) => void
|
||||
transformCallback({ id: 't-1' } as unknown as LexicalNode)
|
||||
|
||||
const createNode = vi.mocked(decoratorTransform).mock.calls[0][2] as () => ErrorMessageBlockNode
|
||||
|
||||
expect(() => createNode()).not.toThrow()
|
||||
expect($createErrorMessageBlockNode).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,143 @@
|
||||
import type { LexicalComposerContextWithEditor } from '@lexical/react/LexicalComposerContext'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { ReactElement } from 'react'
|
||||
import { LexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { render } from '@testing-library/react'
|
||||
import { $insertNodes, COMMAND_PRIORITY_EDITOR } from 'lexical'
|
||||
import {
|
||||
DELETE_ERROR_MESSAGE_COMMAND,
|
||||
ErrorMessageBlock,
|
||||
ErrorMessageBlockNode,
|
||||
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
|
||||
} from './index'
|
||||
import { $createErrorMessageBlockNode } from './node'
|
||||
|
||||
vi.mock('@lexical/utils')
|
||||
vi.mock('lexical', async () => {
|
||||
const actual = await vi.importActual('lexical')
|
||||
return {
|
||||
...actual,
|
||||
$insertNodes: vi.fn(),
|
||||
createCommand: vi.fn(name => name),
|
||||
COMMAND_PRIORITY_EDITOR: 1,
|
||||
}
|
||||
})
|
||||
vi.mock('./node')
|
||||
|
||||
const mockHasNodes = vi.fn()
|
||||
const mockRegisterCommand = vi.fn()
|
||||
|
||||
const mockEditor = {
|
||||
hasNodes: mockHasNodes,
|
||||
registerCommand: mockRegisterCommand,
|
||||
} as unknown as LexicalEditor
|
||||
|
||||
const lexicalContextValue: LexicalComposerContextWithEditor = [
|
||||
mockEditor,
|
||||
{ getTheme: () => undefined },
|
||||
]
|
||||
|
||||
const renderWithLexicalContext = (ui: ReactElement) => {
|
||||
return render(
|
||||
<LexicalComposerContext.Provider value={lexicalContextValue}>
|
||||
{ui}
|
||||
</LexicalComposerContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ErrorMessageBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHasNodes.mockReturnValue(true)
|
||||
mockRegisterCommand.mockReturnValue(vi.fn())
|
||||
vi.mocked(mergeRegister).mockImplementation((...cleanups) => {
|
||||
return () => cleanups.forEach(cleanup => cleanup())
|
||||
})
|
||||
vi.mocked($createErrorMessageBlockNode).mockReturnValue({ id: 'node' } as unknown as ErrorMessageBlockNode)
|
||||
})
|
||||
|
||||
it('should render null and register insert and delete commands', () => {
|
||||
const { container } = renderWithLexicalContext(<ErrorMessageBlock />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
expect(mockHasNodes).toHaveBeenCalledWith([ErrorMessageBlockNode])
|
||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(2)
|
||||
expect(mockRegisterCommand).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
|
||||
expect.any(Function),
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
expect(mockRegisterCommand).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
DELETE_ERROR_MESSAGE_COMMAND,
|
||||
expect.any(Function),
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
expect(ErrorMessageBlock.displayName).toBe('ErrorMessageBlock')
|
||||
})
|
||||
|
||||
it('should throw when ErrorMessageBlockNode is not registered', () => {
|
||||
mockHasNodes.mockReturnValue(false)
|
||||
|
||||
expect(() => renderWithLexicalContext(<ErrorMessageBlock />)).toThrow(
|
||||
'ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor',
|
||||
)
|
||||
})
|
||||
|
||||
it('should insert created node and call onInsert when insert command handler runs', () => {
|
||||
const onInsert = vi.fn()
|
||||
renderWithLexicalContext(<ErrorMessageBlock onInsert={onInsert} />)
|
||||
|
||||
const insertHandler = mockRegisterCommand.mock.calls[0][1] as () => boolean
|
||||
const result = insertHandler()
|
||||
|
||||
expect($createErrorMessageBlockNode).toHaveBeenCalledTimes(1)
|
||||
expect($insertNodes).toHaveBeenCalledWith([{ id: 'node' }])
|
||||
expect(onInsert).toHaveBeenCalledTimes(1)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true on insert command without onInsert callback', () => {
|
||||
renderWithLexicalContext(<ErrorMessageBlock />)
|
||||
|
||||
const insertHandler = mockRegisterCommand.mock.calls[0][1] as () => boolean
|
||||
|
||||
expect(insertHandler()).toBe(true)
|
||||
expect($insertNodes).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onDelete and return true when delete command handler runs', () => {
|
||||
const onDelete = vi.fn()
|
||||
renderWithLexicalContext(<ErrorMessageBlock onDelete={onDelete} />)
|
||||
|
||||
const deleteHandler = mockRegisterCommand.mock.calls[1][1] as () => boolean
|
||||
const result = deleteHandler()
|
||||
|
||||
expect(onDelete).toHaveBeenCalledTimes(1)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true on delete command without onDelete callback', () => {
|
||||
renderWithLexicalContext(<ErrorMessageBlock />)
|
||||
|
||||
const deleteHandler = mockRegisterCommand.mock.calls[1][1] as () => boolean
|
||||
|
||||
expect(deleteHandler()).toBe(true)
|
||||
})
|
||||
|
||||
it('should run merged cleanup on unmount', () => {
|
||||
const insertCleanup = vi.fn()
|
||||
const deleteCleanup = vi.fn()
|
||||
mockRegisterCommand
|
||||
.mockReturnValueOnce(insertCleanup)
|
||||
.mockReturnValueOnce(deleteCleanup)
|
||||
|
||||
const { unmount } = renderWithLexicalContext(<ErrorMessageBlock />)
|
||||
unmount()
|
||||
|
||||
expect(insertCleanup).toHaveBeenCalledTimes(1)
|
||||
expect(deleteCleanup).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,86 @@
|
||||
import type { Klass, LexicalEditor, LexicalNode } from 'lexical'
|
||||
import { createEditor } from 'lexical'
|
||||
import { $createErrorMessageBlockNode, $isErrorMessageBlockNode, ErrorMessageBlockNode } from './node'
|
||||
|
||||
describe('ErrorMessageBlockNode', () => {
|
||||
let editor: LexicalEditor
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
editor = createEditor({
|
||||
nodes: [ErrorMessageBlockNode as unknown as Klass<LexicalNode>],
|
||||
})
|
||||
})
|
||||
|
||||
const runInEditor = (callback: () => void) => {
|
||||
editor.update(callback, { discrete: true })
|
||||
}
|
||||
|
||||
it('should expose correct static type and clone behavior', () => {
|
||||
runInEditor(() => {
|
||||
const original = new ErrorMessageBlockNode('node-key')
|
||||
const cloned = ErrorMessageBlockNode.clone(original)
|
||||
|
||||
expect(ErrorMessageBlockNode.getType()).toBe('error-message-block')
|
||||
expect(cloned).toBeInstanceOf(ErrorMessageBlockNode)
|
||||
expect(cloned).not.toBe(original)
|
||||
expect(cloned.getKey()).toBe(original.getKey())
|
||||
})
|
||||
})
|
||||
|
||||
it('should be inline and provide expected text and json payload', () => {
|
||||
runInEditor(() => {
|
||||
const node = new ErrorMessageBlockNode()
|
||||
|
||||
expect(node.isInline()).toBe(true)
|
||||
expect(node.getTextContent()).toBe('{{#error_message#}}')
|
||||
expect(node.exportJSON()).toEqual({
|
||||
type: 'error-message-block',
|
||||
version: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should create dom with expected classes and never update dom', () => {
|
||||
runInEditor(() => {
|
||||
const node = new ErrorMessageBlockNode()
|
||||
const dom = node.createDOM()
|
||||
|
||||
expect(dom.tagName).toBe('DIV')
|
||||
expect(dom).toHaveClass('inline-flex')
|
||||
expect(dom).toHaveClass('items-center')
|
||||
expect(dom).toHaveClass('align-middle')
|
||||
expect(node.updateDOM()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should decorate using ErrorMessageBlockComponent with node key', () => {
|
||||
runInEditor(() => {
|
||||
const node = new ErrorMessageBlockNode('decorator-key')
|
||||
const decorated = node.decorate()
|
||||
|
||||
expect(decorated.props.nodeKey).toBe('decorator-key')
|
||||
})
|
||||
})
|
||||
|
||||
it('should create and import node instances via helper APIs', () => {
|
||||
runInEditor(() => {
|
||||
const created = $createErrorMessageBlockNode()
|
||||
const imported = ErrorMessageBlockNode.importJSON()
|
||||
|
||||
expect(created).toBeInstanceOf(ErrorMessageBlockNode)
|
||||
expect(imported).toBeInstanceOf(ErrorMessageBlockNode)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return correct type guard values for lexical and non lexical inputs', () => {
|
||||
runInEditor(() => {
|
||||
const node = new ErrorMessageBlockNode()
|
||||
|
||||
expect($isErrorMessageBlockNode(node)).toBe(true)
|
||||
expect($isErrorMessageBlockNode(null)).toBe(false)
|
||||
expect($isErrorMessageBlockNode(undefined)).toBe(false)
|
||||
expect($isErrorMessageBlockNode({} as ErrorMessageBlockNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user