mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
chore: tests for goto anything (#29831)
This commit is contained in:
84
web/app/components/goto-anything/command-selector.spec.tsx
Normal file
84
web/app/components/goto-anything/command-selector.spec.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Command } from 'cmdk'
|
||||
import CommandSelector from './command-selector'
|
||||
import type { ActionItem } from './actions/types'
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: () => '/app',
|
||||
}))
|
||||
|
||||
const slashCommandsMock = [{
|
||||
name: 'zen',
|
||||
description: 'Zen mode',
|
||||
mode: 'direct',
|
||||
isAvailable: () => true,
|
||||
}]
|
||||
|
||||
jest.mock('./actions/commands/registry', () => ({
|
||||
slashCommandRegistry: {
|
||||
getAvailableCommands: () => slashCommandsMock,
|
||||
},
|
||||
}))
|
||||
|
||||
const createActions = (): Record<string, ActionItem> => ({
|
||||
app: {
|
||||
key: '@app',
|
||||
shortcut: '@app',
|
||||
title: 'Apps',
|
||||
search: jest.fn(),
|
||||
description: '',
|
||||
} as ActionItem,
|
||||
plugin: {
|
||||
key: '@plugin',
|
||||
shortcut: '@plugin',
|
||||
title: 'Plugins',
|
||||
search: jest.fn(),
|
||||
description: '',
|
||||
} as ActionItem,
|
||||
})
|
||||
|
||||
describe('CommandSelector', () => {
|
||||
test('should list contextual search actions and notify selection', async () => {
|
||||
const actions = createActions()
|
||||
const onSelect = jest.fn()
|
||||
|
||||
render(
|
||||
<Command>
|
||||
<CommandSelector
|
||||
actions={actions}
|
||||
onCommandSelect={onSelect}
|
||||
searchFilter='app'
|
||||
originalQuery='@app'
|
||||
/>
|
||||
</Command>,
|
||||
)
|
||||
|
||||
const actionButton = screen.getByText('app.gotoAnything.actions.searchApplicationsDesc')
|
||||
await userEvent.click(actionButton)
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith('@app')
|
||||
})
|
||||
|
||||
test('should render slash commands when query starts with slash', async () => {
|
||||
const actions = createActions()
|
||||
const onSelect = jest.fn()
|
||||
|
||||
render(
|
||||
<Command>
|
||||
<CommandSelector
|
||||
actions={actions}
|
||||
onCommandSelect={onSelect}
|
||||
searchFilter='zen'
|
||||
originalQuery='/zen'
|
||||
/>
|
||||
</Command>,
|
||||
)
|
||||
|
||||
const slashItem = await screen.findByText('app.gotoAnything.actions.zenDesc')
|
||||
await userEvent.click(slashItem)
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith('/zen')
|
||||
})
|
||||
})
|
||||
58
web/app/components/goto-anything/context.spec.tsx
Normal file
58
web/app/components/goto-anything/context.spec.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
|
||||
|
||||
let pathnameMock = '/'
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: () => pathnameMock,
|
||||
}))
|
||||
|
||||
let isWorkflowPageMock = false
|
||||
jest.mock('../workflow/constants', () => ({
|
||||
isInWorkflowPage: () => isWorkflowPageMock,
|
||||
}))
|
||||
|
||||
const ContextConsumer = () => {
|
||||
const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext()
|
||||
return (
|
||||
<div data-testid="status">
|
||||
{String(isWorkflowPage)}|{String(isRagPipelinePage)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('GotoAnythingProvider', () => {
|
||||
beforeEach(() => {
|
||||
isWorkflowPageMock = false
|
||||
pathnameMock = '/'
|
||||
})
|
||||
|
||||
test('should set workflow page flag when workflow path detected', async () => {
|
||||
isWorkflowPageMock = true
|
||||
pathnameMock = '/app/123/workflow'
|
||||
|
||||
render(
|
||||
<GotoAnythingProvider>
|
||||
<ContextConsumer />
|
||||
</GotoAnythingProvider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('status')).toHaveTextContent('true|false')
|
||||
})
|
||||
})
|
||||
|
||||
test('should detect RAG pipeline path based on pathname', async () => {
|
||||
pathnameMock = '/datasets/abc/pipeline'
|
||||
|
||||
render(
|
||||
<GotoAnythingProvider>
|
||||
<ContextConsumer />
|
||||
</GotoAnythingProvider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('status')).toHaveTextContent('false|true')
|
||||
})
|
||||
})
|
||||
})
|
||||
173
web/app/components/goto-anything/index.spec.tsx
Normal file
173
web/app/components/goto-anything/index.spec.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from 'react'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import GotoAnything from './index'
|
||||
import type { ActionItem, SearchResult } from './actions/types'
|
||||
|
||||
const routerPush = jest.fn()
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: routerPush,
|
||||
}),
|
||||
usePathname: () => '/',
|
||||
}))
|
||||
|
||||
const keyPressHandlers: Record<string, (event: any) => void> = {}
|
||||
jest.mock('ahooks', () => ({
|
||||
useDebounce: (value: any) => value,
|
||||
useKeyPress: (keys: string | string[], handler: (event: any) => void) => {
|
||||
const keyList = Array.isArray(keys) ? keys : [keys]
|
||||
keyList.forEach((key) => {
|
||||
keyPressHandlers[key] = handler
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
const triggerKeyPress = (combo: string) => {
|
||||
const handler = keyPressHandlers[combo]
|
||||
if (handler) {
|
||||
act(() => {
|
||||
handler({ preventDefault: jest.fn(), target: document.body })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let mockQueryResult = { data: [] as SearchResult[], isLoading: false, isError: false, error: null as Error | null }
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: () => mockQueryResult,
|
||||
}))
|
||||
|
||||
jest.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
const contextValue = { isWorkflowPage: false, isRagPipelinePage: false }
|
||||
jest.mock('./context', () => ({
|
||||
useGotoAnythingContext: () => contextValue,
|
||||
GotoAnythingProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
const createActionItem = (key: ActionItem['key'], shortcut: string): ActionItem => ({
|
||||
key,
|
||||
shortcut,
|
||||
title: `${key} title`,
|
||||
description: `${key} desc`,
|
||||
action: jest.fn(),
|
||||
search: jest.fn(),
|
||||
})
|
||||
|
||||
const actionsMock = {
|
||||
slash: createActionItem('/', '/'),
|
||||
app: createActionItem('@app', '@app'),
|
||||
plugin: createActionItem('@plugin', '@plugin'),
|
||||
}
|
||||
|
||||
const createActionsMock = jest.fn(() => actionsMock)
|
||||
const matchActionMock = jest.fn(() => undefined)
|
||||
const searchAnythingMock = jest.fn(async () => mockQueryResult.data)
|
||||
|
||||
jest.mock('./actions', () => ({
|
||||
__esModule: true,
|
||||
createActions: () => createActionsMock(),
|
||||
matchAction: () => matchActionMock(),
|
||||
searchAnything: () => searchAnythingMock(),
|
||||
}))
|
||||
|
||||
jest.mock('./actions/commands', () => ({
|
||||
SlashCommandProvider: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('./actions/commands/registry', () => ({
|
||||
slashCommandRegistry: {
|
||||
findCommand: () => null,
|
||||
getAvailableCommands: () => [],
|
||||
getAllCommands: () => [],
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/utils/common', () => ({
|
||||
getKeyboardKeyCodeBySystem: () => 'ctrl',
|
||||
isEventTargetInputArea: () => false,
|
||||
isMac: () => false,
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/utils/node-navigation', () => ({
|
||||
selectWorkflowNode: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../plugins/install-plugin/install-from-marketplace', () => (props: { manifest?: { name?: string }, onClose: () => void }) => (
|
||||
<div data-testid="install-modal">
|
||||
<span>{props.manifest?.name}</span>
|
||||
<button onClick={props.onClose}>close</button>
|
||||
</div>
|
||||
))
|
||||
|
||||
describe('GotoAnything', () => {
|
||||
beforeEach(() => {
|
||||
routerPush.mockClear()
|
||||
Object.keys(keyPressHandlers).forEach(key => delete keyPressHandlers[key])
|
||||
mockQueryResult = { data: [], isLoading: false, isError: false, error: null }
|
||||
matchActionMock.mockReset()
|
||||
searchAnythingMock.mockClear()
|
||||
})
|
||||
|
||||
it('should open modal via shortcut and navigate to selected result', async () => {
|
||||
mockQueryResult = {
|
||||
data: [{
|
||||
id: 'app-1',
|
||||
type: 'app',
|
||||
title: 'Sample App',
|
||||
description: 'desc',
|
||||
path: '/apps/1',
|
||||
icon: <div data-testid="icon">🧩</div>,
|
||||
data: {},
|
||||
} as any],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
render(<GotoAnything />)
|
||||
|
||||
triggerKeyPress('ctrl.k')
|
||||
|
||||
const input = await screen.findByPlaceholderText('app.gotoAnything.searchPlaceholder')
|
||||
await userEvent.type(input, 'app')
|
||||
|
||||
const result = await screen.findByText('Sample App')
|
||||
await userEvent.click(result)
|
||||
|
||||
expect(routerPush).toHaveBeenCalledWith('/apps/1')
|
||||
})
|
||||
|
||||
it('should open plugin installer when selecting plugin result', async () => {
|
||||
mockQueryResult = {
|
||||
data: [{
|
||||
id: 'plugin-1',
|
||||
type: 'plugin',
|
||||
title: 'Plugin Item',
|
||||
description: 'desc',
|
||||
path: '',
|
||||
icon: <div />,
|
||||
data: {
|
||||
name: 'Plugin Item',
|
||||
latest_package_identifier: 'pkg',
|
||||
},
|
||||
} as any],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
render(<GotoAnything />)
|
||||
|
||||
triggerKeyPress('ctrl.k')
|
||||
const input = await screen.findByPlaceholderText('app.gotoAnything.searchPlaceholder')
|
||||
await userEvent.type(input, 'plugin')
|
||||
|
||||
const pluginItem = await screen.findByText('Plugin Item')
|
||||
await userEvent.click(pluginItem)
|
||||
|
||||
expect(await screen.findByTestId('install-modal')).toHaveTextContent('Plugin Item')
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,13 @@ import { cleanup } from '@testing-library/react'
|
||||
// Fix for @headlessui/react compatibility with happy-dom
|
||||
// headlessui tries to override focus properties which may be read-only in happy-dom
|
||||
if (typeof window !== 'undefined') {
|
||||
// Provide a minimal animations API polyfill before @headlessui/react boots
|
||||
if (typeof Element !== 'undefined' && !Element.prototype.getAnimations)
|
||||
Element.prototype.getAnimations = () => []
|
||||
|
||||
if (!document.getAnimations)
|
||||
document.getAnimations = () => []
|
||||
|
||||
const ensureWritable = (target: object, prop: string) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, prop)
|
||||
if (descriptor && !descriptor.writable) {
|
||||
|
||||
Reference in New Issue
Block a user