mirror of
https://github.com/langgenius/dify.git
synced 2026-07-26 22:28:32 +08:00
321 lines
9.5 KiB
TypeScript
321 lines
9.5 KiB
TypeScript
import type { SnippetWorkflow } from '@/types/snippet'
|
|
import { renderHook, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useSnippetInit } from '../use-snippet-init'
|
|
|
|
const mockWorkflowStoreSetState = vi.fn()
|
|
const mockSetPublishedAt = vi.fn()
|
|
const mockSetDraftUpdatedAt = vi.fn()
|
|
const mockSetSyncWorkflowDraftHash = vi.fn()
|
|
const mockUseSnippetApiDetail = vi.fn()
|
|
const mockFetchSnippetDraftWorkflow = vi.fn()
|
|
const mockUseSnippetDefaultBlockConfigs = vi.fn()
|
|
const mockUseSnippetPublishedWorkflow = vi.fn()
|
|
|
|
vi.mock('@/app/components/workflow/store', () => ({
|
|
useWorkflowStore: () => ({
|
|
setState: mockWorkflowStoreSetState,
|
|
getState: () => ({
|
|
setDraftUpdatedAt: mockSetDraftUpdatedAt,
|
|
setSyncWorkflowDraftHash: mockSetSyncWorkflowDraftHash,
|
|
setPublishedAt: mockSetPublishedAt,
|
|
}),
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/service/use-snippets', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/service/use-snippets')>()
|
|
|
|
return {
|
|
...actual,
|
|
useSnippetApiDetail: (snippetId: string) => mockUseSnippetApiDetail(snippetId),
|
|
}
|
|
})
|
|
|
|
vi.mock('@/service/use-snippet-workflows', () => ({
|
|
fetchSnippetDraftWorkflow: (snippetId: string) => mockFetchSnippetDraftWorkflow(snippetId),
|
|
useSnippetDefaultBlockConfigs: (snippetId: string, onSuccess?: (data: unknown) => void) =>
|
|
mockUseSnippetDefaultBlockConfigs(snippetId, onSuccess),
|
|
useSnippetPublishedWorkflow: (
|
|
snippetId: string,
|
|
onSuccess?: (data: { created_at: number }) => void,
|
|
) => mockUseSnippetPublishedWorkflow(snippetId, onSuccess),
|
|
}))
|
|
|
|
const createDraftWorkflow = (overrides: Partial<SnippetWorkflow> = {}): SnippetWorkflow => ({
|
|
id: 'draft-1',
|
|
graph: {},
|
|
features: {},
|
|
input_fields: [],
|
|
hash: 'draft-hash',
|
|
created_at: 1_712_300_000,
|
|
updated_at: 1_712_345_678,
|
|
...overrides,
|
|
})
|
|
|
|
describe('useSnippetInit', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
mockUseSnippetApiDetail.mockReturnValue({
|
|
data: {
|
|
id: 'snippet-1',
|
|
name: 'Tone Rewriter',
|
|
description: 'A static snippet mock.',
|
|
type: 'node',
|
|
is_published: false,
|
|
version: '1',
|
|
use_count: 0,
|
|
tags: [],
|
|
input_fields: [],
|
|
created_at: 1_712_300_000,
|
|
created_by: 'user-1',
|
|
updated_at: 1_712_300_000,
|
|
updated_by: 'user-1',
|
|
},
|
|
error: null,
|
|
isLoading: false,
|
|
})
|
|
mockFetchSnippetDraftWorkflow.mockResolvedValue(undefined)
|
|
mockUseSnippetDefaultBlockConfigs.mockReturnValue({
|
|
data: undefined,
|
|
})
|
|
mockUseSnippetPublishedWorkflow.mockReturnValue({
|
|
data: undefined,
|
|
})
|
|
})
|
|
|
|
it('should return snippet detail query result', async () => {
|
|
const { result } = renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
expect(mockUseSnippetApiDetail).toHaveBeenCalledWith('snippet-1')
|
|
expect(mockFetchSnippetDraftWorkflow).toHaveBeenCalledWith('snippet-1')
|
|
expect(result.current.data?.snippet.id).toBe('snippet-1')
|
|
expect(result.current.data?.published.graph.viewport).toEqual({ x: 0, y: 0, zoom: 1 })
|
|
})
|
|
|
|
it('should use draft input_fields for snippet inputs', async () => {
|
|
mockUseSnippetApiDetail.mockReturnValue({
|
|
data: {
|
|
id: 'snippet-1',
|
|
name: 'Tone Rewriter',
|
|
description: 'A static snippet mock.',
|
|
type: 'node',
|
|
is_published: false,
|
|
version: '1',
|
|
use_count: 0,
|
|
tags: [],
|
|
input_fields: [
|
|
{
|
|
label: 'Published field',
|
|
variable: 'published_field',
|
|
type: 'text-input',
|
|
required: true,
|
|
},
|
|
],
|
|
created_at: 1_712_300_000,
|
|
created_by: 'user-1',
|
|
updated_at: 1_712_300_000,
|
|
updated_by: 'user-1',
|
|
},
|
|
error: null,
|
|
isLoading: false,
|
|
})
|
|
mockFetchSnippetDraftWorkflow.mockResolvedValue(
|
|
createDraftWorkflow({
|
|
input_fields: [
|
|
{
|
|
label: 'Draft field',
|
|
variable: 'draft_field',
|
|
type: 'text-input',
|
|
required: true,
|
|
},
|
|
],
|
|
}),
|
|
)
|
|
|
|
const { result } = renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
expect(result.current.data?.draft.inputFields).toEqual([
|
|
{
|
|
label: 'Draft field',
|
|
variable: 'draft_field',
|
|
type: 'text-input',
|
|
required: true,
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should sync draft metadata before returning initialized data', async () => {
|
|
mockFetchSnippetDraftWorkflow.mockResolvedValue(
|
|
createDraftWorkflow({
|
|
hash: 'fetched-draft-hash',
|
|
updated_at: 1_712_345_678,
|
|
graph: {
|
|
nodes: [{ id: 'node-1' }],
|
|
edges: [],
|
|
viewport: { x: 10, y: 20, zoom: 1.2 },
|
|
},
|
|
}),
|
|
)
|
|
|
|
const { result } = renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
expect(mockSetDraftUpdatedAt).toHaveBeenCalledWith(1_712_345_678)
|
|
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('fetched-draft-hash')
|
|
expect(result.current.data?.draft.graph.viewport).toEqual({ x: 10, y: 20, zoom: 1.2 })
|
|
})
|
|
|
|
it('should not return stale draft data while the draft workflow request is pending', () => {
|
|
mockFetchSnippetDraftWorkflow.mockReturnValue(new Promise(() => {}))
|
|
|
|
const { result } = renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
expect(result.current.data).toBeUndefined()
|
|
expect(result.current.isLoading).toBe(true)
|
|
})
|
|
|
|
it('should initialize with empty graph when the draft workflow does not exist', async () => {
|
|
mockFetchSnippetDraftWorkflow.mockResolvedValue(undefined)
|
|
|
|
const { result } = renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
expect(result.current.data?.published.graph).toEqual({
|
|
nodes: [],
|
|
edges: [],
|
|
viewport: { x: 0, y: 0, zoom: 1 },
|
|
})
|
|
})
|
|
|
|
it('should ignore outdated draft workflow response when snippet changes', async () => {
|
|
let resolveFirstDraft: (workflow: SnippetWorkflow) => void = () => {}
|
|
mockFetchSnippetDraftWorkflow.mockImplementation((snippetId: string) => {
|
|
if (snippetId === 'snippet-1') {
|
|
return new Promise<SnippetWorkflow>((resolve) => {
|
|
resolveFirstDraft = resolve
|
|
})
|
|
}
|
|
|
|
return Promise.resolve(
|
|
createDraftWorkflow({
|
|
id: 'draft-2',
|
|
hash: 'snippet-2-hash',
|
|
graph: {
|
|
nodes: [{ id: 'snippet-2-node' }],
|
|
edges: [],
|
|
viewport: { x: 2, y: 2, zoom: 1 },
|
|
},
|
|
}),
|
|
)
|
|
})
|
|
|
|
const { result, rerender } = renderHook(({ snippetId }) => useSnippetInit(snippetId), {
|
|
initialProps: { snippetId: 'snippet-1' },
|
|
})
|
|
|
|
rerender({ snippetId: 'snippet-2' })
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false)
|
|
})
|
|
|
|
resolveFirstDraft(
|
|
createDraftWorkflow({
|
|
hash: 'stale-snippet-1-hash',
|
|
graph: {
|
|
nodes: [{ id: 'stale-node' }],
|
|
edges: [],
|
|
viewport: { x: 1, y: 1, zoom: 1 },
|
|
},
|
|
}),
|
|
)
|
|
await Promise.resolve()
|
|
|
|
expect(result.current.data?.draft.graph.nodes).toEqual([{ id: 'snippet-2-node' }])
|
|
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('snippet-2-hash')
|
|
expect(mockSetSyncWorkflowDraftHash).not.toHaveBeenCalledWith('stale-snippet-1-hash')
|
|
})
|
|
|
|
it('should normalize array default block configs into workflow store state', () => {
|
|
mockUseSnippetDefaultBlockConfigs.mockImplementation(
|
|
(_snippetId: string, onSuccess?: (data: unknown) => void) => {
|
|
onSuccess?.([
|
|
{ type: 'llm', config: { model: 'gpt-4.1' } },
|
|
{ type: 'code', config: { language: 'python3' } },
|
|
])
|
|
return { data: undefined, isLoading: false }
|
|
},
|
|
)
|
|
|
|
renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({
|
|
nodesDefaultConfigs: {
|
|
llm: { model: 'gpt-4.1' },
|
|
code: { language: 'python3' },
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should keep object default block configs as-is', () => {
|
|
mockUseSnippetDefaultBlockConfigs.mockImplementation(
|
|
(_snippetId: string, onSuccess?: (data: unknown) => void) => {
|
|
onSuccess?.({
|
|
llm: { model: 'gpt-4.1' },
|
|
})
|
|
return { data: undefined, isLoading: false }
|
|
},
|
|
)
|
|
|
|
renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({
|
|
nodesDefaultConfigs: {
|
|
llm: { model: 'gpt-4.1' },
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should sync published created_at into workflow store', () => {
|
|
mockUseSnippetPublishedWorkflow.mockImplementation(
|
|
(_snippetId: string, onSuccess?: (data: { created_at: number }) => void) => {
|
|
onSuccess?.({
|
|
created_at: 1_712_345_678,
|
|
})
|
|
return { data: { created_at: 1_712_345_678 }, isLoading: false }
|
|
},
|
|
)
|
|
|
|
renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
expect(mockSetPublishedAt).toHaveBeenCalledWith(1_712_345_678)
|
|
})
|
|
|
|
it('should reset published metadata when the published workflow is unavailable', () => {
|
|
mockUseSnippetPublishedWorkflow.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
})
|
|
|
|
renderHook(() => useSnippetInit('snippet-1'))
|
|
|
|
expect(mockSetPublishedAt).toHaveBeenCalledWith(0)
|
|
})
|
|
})
|