test: add comprehensive unit tests for APIKeyInfoPanel component (#29719)
This commit is contained in:
@@ -0,0 +1,209 @@
|
|||||||
|
import type { RenderOptions } from '@testing-library/react'
|
||||||
|
import { fireEvent, render } from '@testing-library/react'
|
||||||
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
import type { ModalContextState } from '@/context/modal-context'
|
||||||
|
import APIKeyInfoPanel from './index'
|
||||||
|
|
||||||
|
// Mock the modules before importing the functions
|
||||||
|
jest.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/context/modal-context', () => ({
|
||||||
|
useModalContext: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
||||||
|
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
|
||||||
|
|
||||||
|
// Type casting for mocks
|
||||||
|
const mockUseProviderContext = actualUseProviderContext as jest.MockedFunction<typeof actualUseProviderContext>
|
||||||
|
const mockUseModalContext = actualUseModalContext as jest.MockedFunction<typeof actualUseModalContext>
|
||||||
|
|
||||||
|
// Default mock data
|
||||||
|
const defaultProviderContext = {
|
||||||
|
modelProviders: [],
|
||||||
|
refreshModelProviders: noop,
|
||||||
|
textGenerationModelList: [],
|
||||||
|
supportRetrievalMethods: [],
|
||||||
|
isAPIKeySet: false,
|
||||||
|
plan: defaultPlan,
|
||||||
|
isFetchedPlan: false,
|
||||||
|
enableBilling: false,
|
||||||
|
onPlanInfoChanged: noop,
|
||||||
|
enableReplaceWebAppLogo: false,
|
||||||
|
modelLoadBalancingEnabled: false,
|
||||||
|
datasetOperatorEnabled: false,
|
||||||
|
enableEducationPlan: false,
|
||||||
|
isEducationWorkspace: false,
|
||||||
|
isEducationAccount: false,
|
||||||
|
allowRefreshEducationVerify: false,
|
||||||
|
educationAccountExpireAt: null,
|
||||||
|
isLoadingEducationAccountInfo: false,
|
||||||
|
isFetchingEducationAccountInfo: false,
|
||||||
|
webappCopyrightEnabled: false,
|
||||||
|
licenseLimit: {
|
||||||
|
workspace_members: {
|
||||||
|
size: 0,
|
||||||
|
limit: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refreshLicenseLimit: noop,
|
||||||
|
isAllowTransferWorkspace: false,
|
||||||
|
isAllowPublishAsCustomKnowledgePipelineTemplate: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultModalContext: ModalContextState = {
|
||||||
|
setShowAccountSettingModal: noop,
|
||||||
|
setShowApiBasedExtensionModal: noop,
|
||||||
|
setShowModerationSettingModal: noop,
|
||||||
|
setShowExternalDataToolModal: noop,
|
||||||
|
setShowPricingModal: noop,
|
||||||
|
setShowAnnotationFullModal: noop,
|
||||||
|
setShowModelModal: noop,
|
||||||
|
setShowExternalKnowledgeAPIModal: noop,
|
||||||
|
setShowModelLoadBalancingModal: noop,
|
||||||
|
setShowOpeningModal: noop,
|
||||||
|
setShowUpdatePluginModal: noop,
|
||||||
|
setShowEducationExpireNoticeModal: noop,
|
||||||
|
setShowTriggerEventsLimitModal: noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MockOverrides = {
|
||||||
|
providerContext?: Partial<typeof defaultProviderContext>
|
||||||
|
modalContext?: Partial<typeof defaultModalContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type APIKeyInfoPanelRenderOptions = {
|
||||||
|
mockOverrides?: MockOverrides
|
||||||
|
} & Omit<RenderOptions, 'wrapper'>
|
||||||
|
|
||||||
|
// Setup function to configure mocks
|
||||||
|
export function setupMocks(overrides: MockOverrides = {}) {
|
||||||
|
mockUseProviderContext.mockReturnValue({
|
||||||
|
...defaultProviderContext,
|
||||||
|
...overrides.providerContext,
|
||||||
|
})
|
||||||
|
|
||||||
|
mockUseModalContext.mockReturnValue({
|
||||||
|
...defaultModalContext,
|
||||||
|
...overrides.modalContext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom render function
|
||||||
|
export function renderAPIKeyInfoPanel(options: APIKeyInfoPanelRenderOptions = {}) {
|
||||||
|
const { mockOverrides, ...renderOptions } = options
|
||||||
|
|
||||||
|
setupMocks(mockOverrides)
|
||||||
|
|
||||||
|
return render(<APIKeyInfoPanel />, renderOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for common test scenarios
|
||||||
|
export const scenarios = {
|
||||||
|
// Render with API key not set (default)
|
||||||
|
withAPIKeyNotSet: (overrides: MockOverrides = {}) =>
|
||||||
|
renderAPIKeyInfoPanel({
|
||||||
|
mockOverrides: {
|
||||||
|
providerContext: { isAPIKeySet: false },
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Render with API key already set
|
||||||
|
withAPIKeySet: (overrides: MockOverrides = {}) =>
|
||||||
|
renderAPIKeyInfoPanel({
|
||||||
|
mockOverrides: {
|
||||||
|
providerContext: { isAPIKeySet: true },
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Render with mock modal function
|
||||||
|
withMockModal: (mockSetShowAccountSettingModal: jest.Mock, overrides: MockOverrides = {}) =>
|
||||||
|
renderAPIKeyInfoPanel({
|
||||||
|
mockOverrides: {
|
||||||
|
modalContext: { setShowAccountSettingModal: mockSetShowAccountSettingModal },
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test assertions
|
||||||
|
export const assertions = {
|
||||||
|
// Should render main button
|
||||||
|
shouldRenderMainButton: () => {
|
||||||
|
const button = document.querySelector('button.btn-primary')
|
||||||
|
expect(button).toBeInTheDocument()
|
||||||
|
return button
|
||||||
|
},
|
||||||
|
|
||||||
|
// Should not render at all
|
||||||
|
shouldNotRender: (container: HTMLElement) => {
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Should have correct panel styling
|
||||||
|
shouldHavePanelStyling: (panel: HTMLElement) => {
|
||||||
|
expect(panel).toHaveClass(
|
||||||
|
'border-components-panel-border',
|
||||||
|
'bg-components-panel-bg',
|
||||||
|
'relative',
|
||||||
|
'mb-6',
|
||||||
|
'rounded-2xl',
|
||||||
|
'border',
|
||||||
|
'p-8',
|
||||||
|
'shadow-md',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Should have close button
|
||||||
|
shouldHaveCloseButton: (container: HTMLElement) => {
|
||||||
|
const closeButton = container.querySelector('.absolute.right-4.top-4')
|
||||||
|
expect(closeButton).toBeInTheDocument()
|
||||||
|
expect(closeButton).toHaveClass('cursor-pointer')
|
||||||
|
return closeButton
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common user interactions
|
||||||
|
export const interactions = {
|
||||||
|
// Click the main button
|
||||||
|
clickMainButton: () => {
|
||||||
|
const button = document.querySelector('button.btn-primary')
|
||||||
|
if (button) fireEvent.click(button)
|
||||||
|
return button
|
||||||
|
},
|
||||||
|
|
||||||
|
// Click the close button
|
||||||
|
clickCloseButton: (container: HTMLElement) => {
|
||||||
|
const closeButton = container.querySelector('.absolute.right-4.top-4')
|
||||||
|
if (closeButton) fireEvent.click(closeButton)
|
||||||
|
return closeButton
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content keys for assertions
|
||||||
|
export const textKeys = {
|
||||||
|
selfHost: {
|
||||||
|
titleRow1: 'appOverview.apiKeyInfo.selfHost.title.row1',
|
||||||
|
titleRow2: 'appOverview.apiKeyInfo.selfHost.title.row2',
|
||||||
|
setAPIBtn: 'appOverview.apiKeyInfo.setAPIBtn',
|
||||||
|
tryCloud: 'appOverview.apiKeyInfo.tryCloud',
|
||||||
|
},
|
||||||
|
cloud: {
|
||||||
|
trialTitle: 'appOverview.apiKeyInfo.cloud.trial.title',
|
||||||
|
trialDescription: /appOverview\.apiKeyInfo\.cloud\.trial\.description/,
|
||||||
|
setAPIBtn: 'appOverview.apiKeyInfo.setAPIBtn',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup and cleanup utilities
|
||||||
|
export function clearAllMocks() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export mock functions for external access
|
||||||
|
export { mockUseProviderContext, mockUseModalContext, defaultModalContext }
|
||||||
122
web/app/components/app/overview/apikey-info-panel/cloud.spec.tsx
Normal file
122
web/app/components/app/overview/apikey-info-panel/cloud.spec.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { cleanup, screen } from '@testing-library/react'
|
||||||
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
|
import {
|
||||||
|
assertions,
|
||||||
|
clearAllMocks,
|
||||||
|
defaultModalContext,
|
||||||
|
interactions,
|
||||||
|
mockUseModalContext,
|
||||||
|
scenarios,
|
||||||
|
textKeys,
|
||||||
|
} from './apikey-info-panel.test-utils'
|
||||||
|
|
||||||
|
// Mock config for Cloud edition
|
||||||
|
jest.mock('@/config', () => ({
|
||||||
|
IS_CE_EDITION: false, // Test Cloud edition
|
||||||
|
}))
|
||||||
|
|
||||||
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
describe('APIKeyInfoPanel - Cloud Edition', () => {
|
||||||
|
const mockSetShowAccountSettingModal = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearAllMocks()
|
||||||
|
mockUseModalContext.mockReturnValue({
|
||||||
|
...defaultModalContext,
|
||||||
|
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render without crashing when API key is not set', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
assertions.shouldRenderMainButton()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render when API key is already set', () => {
|
||||||
|
const { container } = scenarios.withAPIKeySet()
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render when panel is hidden by user', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
interactions.clickCloseButton(container)
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Cloud Edition Content', () => {
|
||||||
|
it('should display cloud version title', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByText(textKeys.cloud.trialTitle)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display emoji for cloud version', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
expect(container.querySelector('em-emoji')).toBeInTheDocument()
|
||||||
|
expect(container.querySelector('em-emoji')).toHaveAttribute('id', '😀')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display cloud version description', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByText(textKeys.cloud.trialDescription)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render external link for cloud version', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
expect(container.querySelector('a[href="https://cloud.dify.ai/apps"]')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display set API button text', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByText(textKeys.cloud.setAPIBtn)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('User Interactions', () => {
|
||||||
|
it('should call setShowAccountSettingModal when set API button is clicked', () => {
|
||||||
|
scenarios.withMockModal(mockSetShowAccountSettingModal)
|
||||||
|
|
||||||
|
interactions.clickMainButton()
|
||||||
|
|
||||||
|
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||||
|
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide panel when close button is clicked', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
|
||||||
|
interactions.clickCloseButton(container)
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Props and Styling', () => {
|
||||||
|
it('should render button with primary variant', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).toHaveClass('btn-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render panel container with correct classes', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
const panel = container.firstChild as HTMLElement
|
||||||
|
assertions.shouldHavePanelStyling(panel)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('should have button with proper role', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have clickable close button', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
assertions.shouldHaveCloseButton(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
162
web/app/components/app/overview/apikey-info-panel/index.spec.tsx
Normal file
162
web/app/components/app/overview/apikey-info-panel/index.spec.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { cleanup, screen } from '@testing-library/react'
|
||||||
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
|
import {
|
||||||
|
assertions,
|
||||||
|
clearAllMocks,
|
||||||
|
defaultModalContext,
|
||||||
|
interactions,
|
||||||
|
mockUseModalContext,
|
||||||
|
scenarios,
|
||||||
|
textKeys,
|
||||||
|
} from './apikey-info-panel.test-utils'
|
||||||
|
|
||||||
|
// Mock config for CE edition
|
||||||
|
jest.mock('@/config', () => ({
|
||||||
|
IS_CE_EDITION: true, // Test CE edition by default
|
||||||
|
}))
|
||||||
|
|
||||||
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
describe('APIKeyInfoPanel - Community Edition', () => {
|
||||||
|
const mockSetShowAccountSettingModal = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearAllMocks()
|
||||||
|
mockUseModalContext.mockReturnValue({
|
||||||
|
...defaultModalContext,
|
||||||
|
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render without crashing when API key is not set', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
assertions.shouldRenderMainButton()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render when API key is already set', () => {
|
||||||
|
const { container } = scenarios.withAPIKeySet()
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render when panel is hidden by user', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
interactions.clickCloseButton(container)
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Content Display', () => {
|
||||||
|
it('should display self-host title content', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
|
||||||
|
expect(screen.getByText(textKeys.selfHost.titleRow1)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(textKeys.selfHost.titleRow2)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display set API button text', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByText(textKeys.selfHost.setAPIBtn)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render external link with correct href for self-host version', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
const link = container.querySelector('a[href="https://cloud.dify.ai/apps"]')
|
||||||
|
|
||||||
|
expect(link).toBeInTheDocument()
|
||||||
|
expect(link).toHaveAttribute('target', '_blank')
|
||||||
|
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||||
|
expect(link).toHaveTextContent(textKeys.selfHost.tryCloud)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have external link with proper styling for self-host version', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
const link = container.querySelector('a[href="https://cloud.dify.ai/apps"]')
|
||||||
|
|
||||||
|
expect(link).toHaveClass(
|
||||||
|
'mt-2',
|
||||||
|
'flex',
|
||||||
|
'h-[26px]',
|
||||||
|
'items-center',
|
||||||
|
'space-x-1',
|
||||||
|
'p-1',
|
||||||
|
'text-xs',
|
||||||
|
'font-medium',
|
||||||
|
'text-[#155EEF]',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('User Interactions', () => {
|
||||||
|
it('should call setShowAccountSettingModal when set API button is clicked', () => {
|
||||||
|
scenarios.withMockModal(mockSetShowAccountSettingModal)
|
||||||
|
|
||||||
|
interactions.clickMainButton()
|
||||||
|
|
||||||
|
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||||
|
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide panel when close button is clicked', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
|
||||||
|
interactions.clickCloseButton(container)
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Props and Styling', () => {
|
||||||
|
it('should render button with primary variant', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).toHaveClass('btn-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render panel container with correct classes', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
const panel = container.firstChild as HTMLElement
|
||||||
|
assertions.shouldHavePanelStyling(panel)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('State Management', () => {
|
||||||
|
it('should start with visible panel (isShow: true)', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
assertions.shouldRenderMainButton()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle visibility when close button is clicked', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
|
||||||
|
interactions.clickCloseButton(container)
|
||||||
|
assertions.shouldNotRender(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle provider context loading state', () => {
|
||||||
|
scenarios.withAPIKeyNotSet({
|
||||||
|
providerContext: {
|
||||||
|
modelProviders: [],
|
||||||
|
textGenerationModelList: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertions.shouldRenderMainButton()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('should have button with proper role', () => {
|
||||||
|
scenarios.withAPIKeyNotSet()
|
||||||
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have clickable close button', () => {
|
||||||
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
|
assertions.shouldHaveCloseButton(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user