mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
test: add unit tests for plugin detail panel components including action lists, strategy lists, and endpoint management (#31053)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
This commit is contained in:
@ -1874,4 +1874,187 @@ describe('CommonCreateModal', () => {
|
||||
expect(screen.getByTestId('modal')).toHaveAttribute('data-disabled', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeFormType Additional Branches', () => {
|
||||
it('should handle "text" type by returning textInput', () => {
|
||||
const detailWithText = createMockPluginDetail({
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
parameters: [
|
||||
{ name: 'text_type_field', type: 'text' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithText)
|
||||
|
||||
const builder = createMockSubscriptionBuilder()
|
||||
render(<CommonCreateModal {...defaultProps} createType={SupportedCreationMethods.OAUTH} builder={builder} />)
|
||||
|
||||
expect(screen.getByTestId('form-field-text_type_field')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle "secret" type by returning secretInput', () => {
|
||||
const detailWithSecret = createMockPluginDetail({
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
parameters: [
|
||||
{ name: 'secret_type_field', type: 'secret' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithSecret)
|
||||
|
||||
const builder = createMockSubscriptionBuilder()
|
||||
render(<CommonCreateModal {...defaultProps} createType={SupportedCreationMethods.OAUTH} builder={builder} />)
|
||||
|
||||
expect(screen.getByTestId('form-field-secret_type_field')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HandleManualPropertiesChange Provider Fallback', () => {
|
||||
it('should not call updateBuilder when provider is empty', async () => {
|
||||
const detailWithEmptyProvider = createMockPluginDetail({
|
||||
provider: '',
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_schema: [
|
||||
{ name: 'webhook_url', type: 'text', required: true },
|
||||
],
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithEmptyProvider)
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} createType={SupportedCreationMethods.MANUAL} />)
|
||||
|
||||
const input = screen.getByTestId('form-field-webhook_url')
|
||||
fireEvent.change(input, { target: { value: 'https://example.com/webhook' } })
|
||||
|
||||
// updateBuilder should not be called when provider is empty
|
||||
expect(mockUpdateBuilder).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Configuration Step Without Endpoint', () => {
|
||||
it('should handle builder without endpoint', async () => {
|
||||
const builderWithoutEndpoint = createMockSubscriptionBuilder({
|
||||
endpoint: '',
|
||||
})
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} createType={SupportedCreationMethods.MANUAL} builder={builderWithoutEndpoint} />)
|
||||
|
||||
// Component should render without errors
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiKeyStep Flow Additional Coverage', () => {
|
||||
it('should handle verify when no builder created yet', async () => {
|
||||
const detailWithCredentials = createMockPluginDetail({
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_constructor: {
|
||||
credentials_schema: [
|
||||
{ name: 'api_key', type: 'secret', required: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithCredentials)
|
||||
|
||||
// Make createBuilder slow
|
||||
mockCreateBuilder.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1000)))
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} />)
|
||||
|
||||
// Click verify before builder is created
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
// Should still attempt to verify
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auto Parameters Not For APIKEY in Configuration', () => {
|
||||
it('should include parameters for APIKEY in configuration step', async () => {
|
||||
const detailWithParams = createMockPluginDetail({
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_constructor: {
|
||||
credentials_schema: [
|
||||
{ name: 'api_key', type: 'secret', required: true },
|
||||
],
|
||||
parameters: [
|
||||
{ name: 'extra_param', type: 'string', required: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithParams)
|
||||
|
||||
// First verify credentials
|
||||
mockVerifyCredentials.mockImplementation((params, { onSuccess }) => {
|
||||
onSuccess()
|
||||
})
|
||||
|
||||
const builder = createMockSubscriptionBuilder()
|
||||
render(<CommonCreateModal {...defaultProps} builder={builder} />)
|
||||
|
||||
// Click verify
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifyCredentials).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Now in configuration step, should see extra_param
|
||||
expect(screen.getByTestId('form-field-extra_param')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('needCheckValidatedValues Option', () => {
|
||||
it('should pass needCheckValidatedValues: false for manual properties', async () => {
|
||||
const detailWithManualSchema = createMockPluginDetail({
|
||||
declaration: {
|
||||
trigger: {
|
||||
subscription_schema: [
|
||||
{ name: 'webhook_url', type: 'text', required: true },
|
||||
],
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
mockUsePluginStore.mockReturnValue(detailWithManualSchema)
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} createType={SupportedCreationMethods.MANUAL} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateBuilder).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const input = screen.getByTestId('form-field-webhook_url')
|
||||
fireEvent.change(input, { target: { value: 'test' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateBuilder).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1475,4 +1475,213 @@ describe('CreateSubscriptionButton', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== OAuth Callback Edge Cases ====================
|
||||
describe('OAuth Callback - Falsy Data', () => {
|
||||
it('should not open modal when OAuth callback returns falsy data', async () => {
|
||||
// Arrange
|
||||
const { openOAuthPopup } = await import('@/hooks/use-oauth')
|
||||
vi.mocked(openOAuthPopup).mockImplementation((url: string, callback: (data?: unknown) => void) => {
|
||||
callback(undefined) // falsy callback data
|
||||
return null
|
||||
})
|
||||
|
||||
const mockBuilder: TriggerSubscriptionBuilder = {
|
||||
id: 'oauth-builder',
|
||||
name: 'OAuth Builder',
|
||||
provider: 'test-provider',
|
||||
credential_type: TriggerCredentialTypeEnum.Oauth2,
|
||||
credentials: {},
|
||||
endpoint: 'https://test.com',
|
||||
parameters: {},
|
||||
properties: {},
|
||||
workflows_in_use: 0,
|
||||
}
|
||||
|
||||
mockInitiateOAuth.mockImplementation((_provider: string, callbacks: { onSuccess: (response: { authorization_url: string, subscription_builder: TriggerSubscriptionBuilder }) => void }) => {
|
||||
callbacks.onSuccess({
|
||||
authorization_url: 'https://oauth.test.com/authorize',
|
||||
subscription_builder: mockBuilder,
|
||||
})
|
||||
})
|
||||
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.OAUTH, SupportedCreationMethods.MANUAL],
|
||||
}),
|
||||
oauthConfig: createOAuthConfig({ configured: true }),
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Click on OAuth option
|
||||
const oauthOption = screen.getByTestId(`option-${SupportedCreationMethods.OAUTH}`)
|
||||
fireEvent.click(oauthOption)
|
||||
|
||||
// Assert - modal should NOT open because callback data was falsy
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('common-create-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== TriggerProps ClassName Branches ====================
|
||||
describe('TriggerProps ClassName Branches', () => {
|
||||
it('should apply pointer-events-none when non-default method with multiple supported methods', () => {
|
||||
// Arrange - Single APIKEY method (methodType = APIKEY, not DEFAULT_METHOD)
|
||||
// But we need multiple methods to test this branch
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.APIKEY, SupportedCreationMethods.MANUAL],
|
||||
}),
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// The methodType will be DEFAULT_METHOD since multiple methods
|
||||
// This verifies the render doesn't crash with multiple methods
|
||||
expect(screen.getByTestId('custom-select')).toHaveAttribute('data-value', 'default')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Tooltip Disabled Branches ====================
|
||||
describe('Tooltip Disabled Branches', () => {
|
||||
it('should enable tooltip when single method and not at max count', () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.MANUAL],
|
||||
}),
|
||||
subscriptions: [createSubscription()], // Not at max
|
||||
})
|
||||
const props = createDefaultProps({ buttonType: CreateButtonType.ICON_BUTTON })
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - tooltip should be enabled (disabled prop = false for single method)
|
||||
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable tooltip when multiple methods and not at max count', () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.MANUAL, SupportedCreationMethods.APIKEY],
|
||||
}),
|
||||
subscriptions: [createSubscription()], // Not at max
|
||||
})
|
||||
const props = createDefaultProps({ buttonType: CreateButtonType.ICON_BUTTON })
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - tooltip should be disabled (neither single method nor at max)
|
||||
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Tooltip PopupContent Branches ====================
|
||||
describe('Tooltip PopupContent Branches', () => {
|
||||
it('should show max count message when at max subscriptions', () => {
|
||||
// Arrange
|
||||
const maxSubscriptions = createMaxSubscriptions()
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.MANUAL],
|
||||
}),
|
||||
subscriptions: maxSubscriptions,
|
||||
})
|
||||
const props = createDefaultProps({ buttonType: CreateButtonType.ICON_BUTTON })
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - component renders with max subscriptions
|
||||
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show method description when not at max', () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.MANUAL],
|
||||
}),
|
||||
subscriptions: [], // Not at max
|
||||
})
|
||||
const props = createDefaultProps({ buttonType: CreateButtonType.ICON_BUTTON })
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - component renders without max subscriptions
|
||||
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Provider Info Fallbacks ====================
|
||||
describe('Provider Info Fallbacks', () => {
|
||||
it('should handle undefined supported_creation_methods', () => {
|
||||
// Arrange - providerInfo with undefined supported_creation_methods
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: {
|
||||
...createProviderInfo(),
|
||||
supported_creation_methods: undefined as unknown as SupportedCreationMethods[],
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - should render null when supported methods fallback to empty
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('should handle providerInfo with null supported_creation_methods', () => {
|
||||
// Arrange
|
||||
mockProviderInfo = { data: { ...createProviderInfo(), supported_creation_methods: null as unknown as SupportedCreationMethods[] } }
|
||||
mockOAuthConfig = { data: undefined, refetch: vi.fn() }
|
||||
mockStoreDetail = createStoreDetail()
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert - should render null
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Method Type Logic ====================
|
||||
describe('Method Type Logic', () => {
|
||||
it('should use single method as methodType when only one supported', () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
storeDetail: createStoreDetail(),
|
||||
providerInfo: createProviderInfo({
|
||||
supported_creation_methods: [SupportedCreationMethods.APIKEY],
|
||||
}),
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<CreateSubscriptionButton {...props} />)
|
||||
|
||||
// Assert
|
||||
const customSelect = screen.getByTestId('custom-select')
|
||||
expect(customSelect).toHaveAttribute('data-value', SupportedCreationMethods.APIKEY)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1240,4 +1240,60 @@ describe('OAuthClientSettingsModal', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('OAuth Client Schema Params Fallback', () => {
|
||||
it('should handle schema when params is truthy but schema name not in params', () => {
|
||||
const configWithSchemaNotInParams = createMockOAuthConfig({
|
||||
system_configured: false,
|
||||
custom_enabled: true,
|
||||
params: {
|
||||
client_id: 'test-id',
|
||||
client_secret: 'test-secret',
|
||||
},
|
||||
oauth_client_schema: [
|
||||
{ name: 'client_id', type: 'text-input' as unknown, required: true, label: { 'en-US': 'Client ID' } as unknown },
|
||||
{ name: 'client_secret', type: 'secret-input' as unknown, required: true, label: { 'en-US': 'Client Secret' } as unknown },
|
||||
{ name: 'extra_field', type: 'text-input' as unknown, required: false, label: { 'en-US': 'Extra' } as unknown },
|
||||
] as TriggerOAuthConfig['oauth_client_schema'],
|
||||
})
|
||||
|
||||
render(<OAuthClientSettingsModal {...defaultProps} oauthConfig={configWithSchemaNotInParams} />)
|
||||
|
||||
// extra_field should be rendered but without default value
|
||||
const extraInput = screen.getByTestId('form-field-extra_field') as HTMLInputElement
|
||||
expect(extraInput.defaultValue).toBe('')
|
||||
})
|
||||
|
||||
it('should handle oauth_client_schema with undefined params', () => {
|
||||
const configWithUndefinedParams = createMockOAuthConfig({
|
||||
system_configured: false,
|
||||
custom_enabled: true,
|
||||
params: undefined as unknown as TriggerOAuthConfig['params'],
|
||||
oauth_client_schema: [
|
||||
{ name: 'client_id', type: 'text-input' as unknown, required: true, label: { 'en-US': 'Client ID' } as unknown },
|
||||
] as TriggerOAuthConfig['oauth_client_schema'],
|
||||
})
|
||||
|
||||
render(<OAuthClientSettingsModal {...defaultProps} oauthConfig={configWithUndefinedParams} />)
|
||||
|
||||
// Form should not render because params is undefined (schema condition fails)
|
||||
expect(screen.queryByTestId('base-form')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle oauth_client_schema with null params', () => {
|
||||
const configWithNullParams = createMockOAuthConfig({
|
||||
system_configured: false,
|
||||
custom_enabled: true,
|
||||
params: null as unknown as TriggerOAuthConfig['params'],
|
||||
oauth_client_schema: [
|
||||
{ name: 'client_id', type: 'text-input' as unknown, required: true, label: { 'en-US': 'Client ID' } as unknown },
|
||||
] as TriggerOAuthConfig['oauth_client_schema'],
|
||||
})
|
||||
|
||||
render(<OAuthClientSettingsModal {...defaultProps} oauthConfig={configWithNullParams} />)
|
||||
|
||||
// Form should not render because params is null
|
||||
expect(screen.queryByTestId('base-form')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user