Files
dify/web/app/components/workflow/skill/hooks/use-inline-create-node.spec.tsx
yyh 799d0c0d0b feat(skill-editor): auto-focus editor on file creation and improve tree-tab sync
Add editorAutoFocusFileId state to automatically focus the editor when
a new text file is created. Improve tree-tab synchronization by adding
syncSignal/isTreeLoading guards, deduplicating rAF calls, and skipping
redundant select/openParents operations when the node is already active.
2026-02-06 14:21:33 +08:00

124 lines
3.7 KiB
TypeScript

import type { ReactNode } from 'react'
import type { TreeApi } from 'react-arborist'
import type { TreeNodeData } from '../type'
import type { App, AppSSO } from '@/types/app'
import { act, renderHook } from '@testing-library/react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { WorkflowContext } from '@/app/components/workflow/context'
import { createWorkflowStore } from '@/app/components/workflow/store'
import { START_TAB_ID } from '../constants'
import { useInlineCreateNode } from './use-inline-create-node'
const {
mockUploadMutateAsync,
mockCreateFolderMutateAsync,
mockRenameMutateAsync,
mockEmitTreeUpdate,
mockToastNotify,
} = vi.hoisted(() => ({
mockUploadMutateAsync: vi.fn(),
mockCreateFolderMutateAsync: vi.fn(),
mockRenameMutateAsync: vi.fn(),
mockEmitTreeUpdate: vi.fn(),
mockToastNotify: vi.fn(),
}))
vi.mock('@/service/use-app-asset', () => ({
useUploadFileWithPresignedUrl: () => ({
mutateAsync: mockUploadMutateAsync,
}),
useCreateAppAssetFolder: () => ({
mutateAsync: mockCreateFolderMutateAsync,
}),
useRenameAppAssetNode: () => ({
mutateAsync: mockRenameMutateAsync,
}),
}))
vi.mock('./use-skill-tree-collaboration', () => ({
useSkillTreeUpdateEmitter: () => mockEmitTreeUpdate,
}))
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: mockToastNotify,
},
}))
const createWrapper = (store: ReturnType<typeof createWorkflowStore>) => {
return ({ children }: { children: ReactNode }) => (
<WorkflowContext.Provider value={store}>
{children}
</WorkflowContext.Provider>
)
}
describe('useInlineCreateNode', () => {
beforeEach(() => {
vi.clearAllMocks()
useAppStore.setState({
appDetail: { id: 'app-1' } as App & Partial<AppSSO>,
})
})
it('should open created text file tab with editor auto focus intent', async () => {
const store = createWorkflowStore({})
const treeRef = { current: null } as React.RefObject<TreeApi<TreeNodeData> | null>
mockUploadMutateAsync.mockResolvedValue({
id: 'file-1',
extension: 'md',
})
store.getState().startCreateNode('file', null)
const pendingId = store.getState().pendingCreateNode?.id as string
const { result } = renderHook(() => useInlineCreateNode({
treeRef,
treeChildren: [],
}), { wrapper: createWrapper(store) })
await act(async () => {
await result.current.handleRename({
id: pendingId,
name: 'README.md',
})
})
expect(mockUploadMutateAsync).toHaveBeenCalledTimes(1)
expect(store.getState().activeTabId).toBe('file-1')
expect(store.getState().editorAutoFocusFileId).toBe('file-1')
expect(store.getState().openTabIds).toEqual(['file-1'])
expect(store.getState().pendingCreateNode).toBeNull()
})
it('should not open tab for non-text-like created files', async () => {
const store = createWorkflowStore({})
const treeRef = { current: null } as React.RefObject<TreeApi<TreeNodeData> | null>
mockUploadMutateAsync.mockResolvedValue({
id: 'file-2',
extension: 'png',
})
store.getState().startCreateNode('file', null)
const pendingId = store.getState().pendingCreateNode?.id as string
const { result } = renderHook(() => useInlineCreateNode({
treeRef,
treeChildren: [],
}), { wrapper: createWrapper(store) })
await act(async () => {
await result.current.handleRename({
id: pendingId,
name: 'image.png',
})
})
expect(mockUploadMutateAsync).toHaveBeenCalledTimes(1)
expect(store.getState().activeTabId).toBe(START_TAB_ID)
expect(store.getState().editorAutoFocusFileId).toBeNull()
expect(store.getState().openTabIds).toEqual([])
expect(store.getState().pendingCreateNode).toBeNull()
})
})