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:
Coding On Star
2026-01-19 14:40:32 +08:00
committed by GitHub
parent 9f09414dbe
commit 92dbc94f2f
25 changed files with 6134 additions and 0 deletions

View File

@ -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()
})
})
})
})

View File

@ -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)
})
})
})

View File

@ -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()
})
})
})