mirror of
https://github.com/langgenius/dify.git
synced 2026-03-20 22:17:58 +08:00
Merge branch 'feat/model-plugins-implementing' into deploy/dev
This commit is contained in:
4
.github/workflows/web-tests.yml
vendored
4
.github/workflows/web-tests.yml
vendored
@ -17,8 +17,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
shardIndex: [1, 2, 3, 4, 5, 6]
|
||||
shardTotal: [6]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
@ -33,7 +33,7 @@ const createWorkflowNodesMap = (title = 'Node One'): WorkflowNodesMap => ({
|
||||
})
|
||||
|
||||
const hasErrorIcon = (container: HTMLElement) => {
|
||||
return container.querySelector('svg.text-text-destructive') !== null
|
||||
return container.querySelector('svg.text-text-warning') !== null
|
||||
}
|
||||
|
||||
const renderVariableBlock = (props: {
|
||||
|
||||
@ -127,6 +127,14 @@ vi.mock('@/service/use-common', () => ({
|
||||
],
|
||||
},
|
||||
}),
|
||||
useCurrentWorkspace: () => ({
|
||||
data: {
|
||||
trial_credits: 1000,
|
||||
trial_credits_used: 100,
|
||||
next_credit_reset_date: undefined,
|
||||
},
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
|
||||
@ -30,6 +30,21 @@ vi.mock('@/context/app-context', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/service/use-common')>('@/service/use-common')
|
||||
return {
|
||||
...actual,
|
||||
useCurrentWorkspace: () => ({
|
||||
data: {
|
||||
trial_credits: 1000,
|
||||
trial_credits_used: 100,
|
||||
next_credit_reset_date: undefined,
|
||||
},
|
||||
isPending: false,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock model-provider-page hooks
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: () => ({ data: [], mutate: vi.fn(), isLoading: false }),
|
||||
|
||||
@ -43,13 +43,23 @@ const InstallFromMarketplace = ({
|
||||
<div className="mb-2">
|
||||
<Divider className="!mt-4 h-px" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex cursor-pointer items-center gap-1 text-text-primary system-md-semibold" onClick={() => setCollapse(!collapse)}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-left text-text-primary system-md-semibold"
|
||||
onClick={() => setCollapse(prev => !prev)}
|
||||
aria-expanded={!collapse}
|
||||
>
|
||||
<span className={cn('i-ri-arrow-down-s-line h-4 w-4', collapse && '-rotate-90')} />
|
||||
{t('modelProvider.installProvider', { ns: 'common' })}
|
||||
</div>
|
||||
</button>
|
||||
<div className="mb-2 flex items-center pt-2">
|
||||
<span className="pr-1 text-text-tertiary system-sm-regular">{t('modelProvider.discoverMore', { ns: 'common' })}</span>
|
||||
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className="inline-flex items-center text-text-accent system-sm-medium">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getMarketplaceUrl('', { theme })}
|
||||
className="inline-flex items-center text-text-accent system-sm-medium"
|
||||
>
|
||||
{t('marketplace.difyMarketplace', { ns: 'plugin' })}
|
||||
<span className="i-ri-arrow-right-up-line h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { Model, ModelItem } from '../declarations'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { DefaultModel, Model, ModelItem } from '../declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
@ -7,16 +9,20 @@ import {
|
||||
} from '../declarations'
|
||||
import ModelSelector from './index'
|
||||
|
||||
vi.mock('./model-trigger', () => ({
|
||||
default: () => <div>model-trigger</div>,
|
||||
}))
|
||||
vi.mock('./model-selector-trigger', () => ({
|
||||
default: ({
|
||||
currentProvider,
|
||||
currentModel,
|
||||
defaultModel,
|
||||
}: { currentProvider?: Model, currentModel?: ModelItem, defaultModel?: DefaultModel }) => {
|
||||
if (currentProvider && currentModel)
|
||||
return <div>model-trigger</div>
|
||||
|
||||
vi.mock('./deprecated-model-trigger', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <div>{`deprecated:${modelName}`}</div>,
|
||||
}))
|
||||
if (defaultModel)
|
||||
return <div>{`deprecated:${defaultModel.model}`}</div>
|
||||
|
||||
vi.mock('./empty-trigger', () => ({
|
||||
default: () => <div>empty-trigger</div>,
|
||||
return <div>empty-trigger</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./popup', () => ({
|
||||
@ -52,24 +58,43 @@ const makeModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createTestQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithQueryClient = (node: ReactNode) => {
|
||||
const queryClient = createTestQueryClient()
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{node}
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ModelSelector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should toggle popup and close it after selecting a model', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} />)
|
||||
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
const triggerButton = screen.getByRole('button', { name: 'empty-trigger' })
|
||||
|
||||
fireEvent.click(triggerButton)
|
||||
expect(triggerButton).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('select')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument()
|
||||
expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('should call onSelect when provided', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(<ModelSelector modelList={[makeModel()]} onSelect={onSelect} />)
|
||||
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} onSelect={onSelect} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
@ -78,24 +103,26 @@ describe('ModelSelector', () => {
|
||||
})
|
||||
|
||||
it('should close popup when popup requests hide', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} />)
|
||||
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
const triggerButton = screen.getByRole('button', { name: 'empty-trigger' })
|
||||
fireEvent.click(triggerButton)
|
||||
expect(triggerButton).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('hide')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('hide'))
|
||||
expect(screen.queryByText('hide')).not.toBeInTheDocument()
|
||||
expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('should not open popup when readonly', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} readonly />)
|
||||
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} readonly />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render deprecated trigger when defaultModel is not in list', () => {
|
||||
const { rerender } = render(
|
||||
const { unmount } = renderWithQueryClient(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: 'openai', model: 'missing-model' }}
|
||||
modelList={[makeModel()]}
|
||||
@ -104,7 +131,8 @@ describe('ModelSelector', () => {
|
||||
|
||||
expect(screen.getByText('deprecated:missing-model')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
unmount()
|
||||
renderWithQueryClient(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: '', model: '' }}
|
||||
modelList={[makeModel()]}
|
||||
@ -114,7 +142,7 @@ describe('ModelSelector', () => {
|
||||
})
|
||||
|
||||
it('should render model trigger when defaultModel matches', () => {
|
||||
render(
|
||||
renderWithQueryClient(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: 'openai', model: 'gpt-4' }}
|
||||
modelList={[makeModel()]}
|
||||
|
||||
@ -22,15 +22,6 @@ vi.mock('@/utils/tool-call', () => ({
|
||||
supportFunctionCall: mockSupportFunctionCall,
|
||||
}))
|
||||
|
||||
const mockCloseActiveTooltip = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/app/components/base/tooltip/TooltipManager', () => ({
|
||||
tooltipManager: {
|
||||
closeActiveTooltip: mockCloseActiveTooltip,
|
||||
register: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
type MockMarketplacePlugin = {
|
||||
plugin_id: string
|
||||
latest_package_identifier: string
|
||||
@ -231,12 +222,20 @@ describe('Popup', () => {
|
||||
expect(screen.getByText('No model found for \u201C\u201D')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should match labels from other languages when current language key is missing', () => {
|
||||
it('should match model labels from fallback languages when current language key is missing', () => {
|
||||
mockLanguage = 'fr_FR'
|
||||
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
modelList={[
|
||||
makeModel({
|
||||
models: [
|
||||
makeModelItem({
|
||||
label: { en_US: 'OpenAI GPT', zh_Hans: 'OpenAI GPT' },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
@ -244,25 +243,12 @@ describe('Popup', () => {
|
||||
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
|
||||
{ target: { value: 'gpt' } },
|
||||
{ target: { value: 'openai' } },
|
||||
)
|
||||
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close tooltip on scroll', () => {
|
||||
const { container } = render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.scroll(container.firstElementChild as HTMLElement)
|
||||
expect(mockCloseActiveTooltip).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open provider settings when clicking footer link', () => {
|
||||
const onHide = vi.fn()
|
||||
render(
|
||||
|
||||
@ -34,7 +34,13 @@ export default function CreditsExhaustedAlert({ hasApiKeyFallback }: CreditsExha
|
||||
i18nKey={descriptionKey}
|
||||
ns="common"
|
||||
components={{
|
||||
upgradeLink: <span className="cursor-pointer text-text-accent system-xs-medium" onClick={setShowPricingModal} />,
|
||||
upgradeLink: (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-left text-text-accent system-xs-medium"
|
||||
onClick={() => setShowPricingModal()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -170,9 +170,13 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
}))
|
||||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.example.com',
|
||||
}))
|
||||
vi.mock('@/config', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/config')>('@/config')
|
||||
return {
|
||||
...actual,
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.example.com',
|
||||
}
|
||||
})
|
||||
|
||||
// Mock mitt context
|
||||
vi.mock('@/context/mitt-context', () => ({
|
||||
|
||||
@ -9,40 +9,6 @@ import ModelParameterModal from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
// Mock shared state for portal
|
||||
let mockPortalOpenState = false
|
||||
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
|
||||
mockPortalOpenState = open || false
|
||||
return (
|
||||
<div data-testid="portal-elem" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick: () => void, className?: string }) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
if (!mockPortalOpenState)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="portal-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock provider context
|
||||
const mockProviderContextValue = {
|
||||
isAPIKeySet: true,
|
||||
@ -87,6 +53,8 @@ vi.mock('@/utils/completion-params', () => ({
|
||||
fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args),
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.spyOn(Toast, 'notify')
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
default: ({ defaultModel, modelList, scopeFeatures, onSelect }: {
|
||||
@ -108,30 +76,33 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger', () => ({
|
||||
default: ({ disabled, hasDeprecated, modelDisabled, currentProvider, currentModel, providerName, modelId, isInWorkflow }: {
|
||||
disabled?: boolean
|
||||
hasDeprecated?: boolean
|
||||
modelDisabled?: boolean
|
||||
default: ({ currentProvider, currentModel, providerName, modelId, isInWorkflow }: {
|
||||
currentProvider?: Model
|
||||
currentModel?: ModelItem
|
||||
providerName?: string
|
||||
modelId?: string
|
||||
isInWorkflow?: boolean
|
||||
}) => (
|
||||
<div
|
||||
data-testid="trigger"
|
||||
data-disabled={disabled}
|
||||
data-has-deprecated={hasDeprecated}
|
||||
data-model-disabled={modelDisabled}
|
||||
data-provider={providerName}
|
||||
data-model={modelId}
|
||||
data-in-workflow={isInWorkflow}
|
||||
data-has-current-provider={!!currentProvider}
|
||||
data-has-current-model={!!currentModel}
|
||||
>
|
||||
Trigger
|
||||
</div>
|
||||
),
|
||||
}) => {
|
||||
const hasDeprecated = !currentProvider || !currentModel
|
||||
const modelDisabled = currentModel?.status !== ModelStatusEnum.active
|
||||
const disabled = !mockProviderContextValue.isAPIKeySet || hasDeprecated || modelDisabled
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="trigger"
|
||||
data-disabled={disabled}
|
||||
data-has-deprecated={hasDeprecated}
|
||||
data-model-disabled={modelDisabled}
|
||||
data-provider={providerName}
|
||||
data-model={modelId}
|
||||
data-in-workflow={isInWorkflow}
|
||||
data-has-current-provider={!!currentProvider}
|
||||
data-has-current-model={!!currentModel}
|
||||
>
|
||||
Trigger
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/agent-model-trigger', () => ({
|
||||
@ -273,7 +244,7 @@ const setupModelLists = (config: {
|
||||
describe('ModelParameterModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPortalOpenState = false
|
||||
mockToastNotify.mockReturnValue({})
|
||||
mockProviderContextValue.isAPIKeySet = true
|
||||
mockProviderContextValue.modelProviders = []
|
||||
setupModelLists()
|
||||
@ -356,7 +327,7 @@ describe('ModelParameterModal', () => {
|
||||
render(<ModelParameterModal {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render model selector inside portal content when open', async () => {
|
||||
@ -365,13 +336,12 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -405,12 +375,11 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const content = screen.getByTestId('portal-content')
|
||||
expect(content.querySelector('.custom-popup-class')).toBeInTheDocument()
|
||||
expect(document.querySelector('.custom-popup-class')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -422,7 +391,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
const selector = screen.getByTestId('model-selector')
|
||||
@ -438,13 +407,13 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -454,15 +423,15 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<ModelParameterModal {...props} />)
|
||||
expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Force a re-render to ensure state is stable
|
||||
rerender(<ModelParameterModal {...props} />)
|
||||
|
||||
// Assert - open state should remain false due to readonly
|
||||
expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -474,7 +443,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -489,7 +458,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -512,7 +481,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -530,7 +499,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -547,7 +516,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -564,7 +533,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -581,7 +550,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -598,7 +567,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -615,7 +584,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -632,7 +601,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -831,7 +800,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('model-selector'))
|
||||
@ -856,7 +825,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('model-selector'))
|
||||
@ -888,7 +857,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('model-selector'))
|
||||
@ -915,7 +884,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByTestId('model-selector'))
|
||||
@ -951,7 +920,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
const panel = screen.getByTestId('llm-params-panel')
|
||||
@ -988,7 +957,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
const panel = screen.getByTestId('tts-params-panel')
|
||||
@ -1025,7 +994,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1051,7 +1020,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1077,7 +1046,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1104,12 +1073,11 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const content = screen.getByTestId('portal-content')
|
||||
expect(content.querySelector('.bg-divider-subtle')).toBeInTheDocument()
|
||||
expect(document.querySelector('.bg-divider-subtle')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1146,7 +1114,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1185,7 +1153,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1264,7 +1232,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@ -1280,7 +1248,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert - defaultModel is created with undefined provider
|
||||
await waitFor(() => {
|
||||
@ -1297,7 +1265,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert - defaultModel is created with undefined model
|
||||
await waitFor(() => {
|
||||
@ -1314,7 +1282,7 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
// Assert - when defaultModel is undefined, attribute is not set (returns null)
|
||||
await waitFor(() => {
|
||||
@ -1350,14 +1318,13 @@ describe('ModelParameterModal', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<ModelParameterModal {...props} />)
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByTestId('trigger'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('model-selector')).toHaveAttribute('data-model-list-count', '1')
|
||||
})
|
||||
|
||||
// Rerender with different scope
|
||||
mockPortalOpenState = true
|
||||
rerender(<ModelParameterModal {...props} scope={ModelTypeEnum.textEmbedding} />)
|
||||
|
||||
// Assert
|
||||
@ -1398,7 +1365,7 @@ describe('ModelParameterModal', () => {
|
||||
render(<ModelParameterModal {...props} />)
|
||||
|
||||
// Assert
|
||||
const trigger = screen.getByTestId('portal-trigger')
|
||||
const trigger = screen.getByTestId('trigger')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -191,7 +191,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
<div className="relative">
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<div className="block">
|
||||
<button type="button" className="block w-full border-none bg-transparent p-0 text-left [color:inherit] [font:inherit]">
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger({
|
||||
@ -224,7 +224,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
|
||||
@ -46,6 +46,7 @@ const PluginItem: FC<PluginItemProps> = ({
|
||||
</div>
|
||||
{onClear && (
|
||||
<button
|
||||
type="button"
|
||||
className="hidden h-6 w-6 shrink-0 items-center justify-center rounded-md hover:bg-state-base-hover-alt group-hover/item:flex"
|
||||
onClick={onClear}
|
||||
>
|
||||
|
||||
@ -99,6 +99,12 @@ const UpdatePluginModal: FC<Props> = ({
|
||||
}) as Awaited<ReturnType<typeof updateFromMarketPlace>> & FailedUpgradeResponse
|
||||
|
||||
if (response.task?.status === TaskStatus.failed) {
|
||||
const failedPlugin = response.task.plugins?.find(plugin => plugin.plugin_unique_identifier === targetPackageInfo.id)
|
||||
?? response.task.plugins?.[0]
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: failedPlugin?.message || t('error', { ns: 'common' }),
|
||||
})
|
||||
setUploadStep(UploadStep.notStarted)
|
||||
return
|
||||
}
|
||||
@ -132,7 +138,7 @@ const UpdatePluginModal: FC<Props> = ({
|
||||
}
|
||||
if (uploadStep === UploadStep.installed)
|
||||
onSave()
|
||||
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
|
||||
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, t, targetPackageInfo.id])
|
||||
|
||||
const { mutateAsync } = useRemoveAutoUpgrade()
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
|
||||
@ -74,6 +74,7 @@ const PluginVersionPicker: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
disabled={disabled}
|
||||
className={cn('inline-flex cursor-pointer items-center', disabled && 'cursor-default')}
|
||||
>
|
||||
{trigger}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ChecklistItem } from '../../hooks/use-checklist'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { Popover, PopoverContent } from '@/app/components/base/ui/popover'
|
||||
import { useStore as usePluginDependencyStore } from '../../plugin-dependency/store'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { ChecklistPluginGroup } from './plugin-group'
|
||||
@ -17,6 +18,20 @@ const createChecklistItem = (overrides: Partial<ChecklistItem> = {}): ChecklistI
|
||||
})
|
||||
|
||||
describe('ChecklistPluginGroup', () => {
|
||||
const getInstallButton = () => {
|
||||
return screen.getByText('workflow.nodes.agent.pluginInstaller.install').closest('button') as HTMLButtonElement
|
||||
}
|
||||
|
||||
const renderInPopover = (items: ChecklistItem[]) => {
|
||||
return render(
|
||||
<Popover open>
|
||||
<PopoverContent>
|
||||
<ChecklistPluginGroup items={items} />
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
usePluginDependencyStore.setState({ dependencies: [] })
|
||||
})
|
||||
@ -28,9 +43,9 @@ describe('ChecklistPluginGroup', () => {
|
||||
createChecklistItem({ id: 'node-3', pluginUniqueIdentifier: 'langgenius/another-plugin:2.0.0@sha256' }),
|
||||
]
|
||||
|
||||
render(<ChecklistPluginGroup items={items} />)
|
||||
renderInPopover(items)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
fireEvent.click(getInstallButton())
|
||||
|
||||
expect(usePluginDependencyStore.getState().dependencies).toEqual([
|
||||
{
|
||||
@ -53,9 +68,9 @@ describe('ChecklistPluginGroup', () => {
|
||||
})
|
||||
|
||||
it('should keep install button disabled when no identifier is available', () => {
|
||||
render(<ChecklistPluginGroup items={[createChecklistItem({ pluginUniqueIdentifier: undefined })]} />)
|
||||
renderInPopover([createChecklistItem({ pluginUniqueIdentifier: undefined })])
|
||||
|
||||
const installButton = screen.getByRole('button')
|
||||
const installButton = getInstallButton()
|
||||
expect(installButton).toBeDisabled()
|
||||
|
||||
fireEvent.click(installButton)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import type { ChecklistItem } from '../../hooks/use-checklist'
|
||||
import type { BlockEnum } from '../../types'
|
||||
import type { Dependency } from '@/app/components/plugins/types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PopoverClose } from '@/app/components/base/ui/popover'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { useStore as usePluginDependencyStore } from '../../plugin-dependency/store'
|
||||
import { ItemIndicator } from './item-indicator'
|
||||
@ -46,8 +46,7 @@ export const ChecklistPluginGroup = memo(({
|
||||
})
|
||||
}, [identifiers])
|
||||
|
||||
const handleInstallAll: MouseEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
const handleInstallAll = () => {
|
||||
if (dependencies.length === 0)
|
||||
return
|
||||
const { setDependencies } = usePluginDependencyStore.getState()
|
||||
@ -63,14 +62,18 @@ export const ChecklistPluginGroup = memo(({
|
||||
<span className="min-w-0 grow truncate text-sm font-medium leading-5 text-text-primary">
|
||||
{t('nodes.common.pluginsNotInstalled', { ns: 'workflow', count: items.length })}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleInstallAll}
|
||||
disabled={dependencies.length === 0}
|
||||
<PopoverClose
|
||||
render={(
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleInstallAll}
|
||||
disabled={dependencies.length === 0}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{t('nodes.agent.pluginInstaller.install', { ns: 'workflow' })}
|
||||
</Button>
|
||||
</PopoverClose>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{items.map(item => (
|
||||
|
||||
@ -4568,11 +4568,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/model-provider-page/model-name/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/model-provider-page/model-parameter-modal/agent-model-trigger.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
|
||||
Reference in New Issue
Block a user