test: header account about, account setting and account dropdown (#32283)

This commit is contained in:
mahammadasim
2026-02-23 09:45:57 +05:30
committed by GitHub
parent aad980f267
commit e4ddf07194
15 changed files with 2433 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
import type { LangGeniusVersionResponse } from '@/models/common'
import type { SystemFeatures } from '@/types/feature'
import { fireEvent, render, screen } from '@testing-library/react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import AccountAbout from './index'
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
}))
let mockIsCEEdition = false
vi.mock('@/config', () => ({
get IS_CE_EDITION() { return mockIsCEEdition },
}))
type GlobalPublicStore = {
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
describe('AccountAbout', () => {
const mockVersionInfo: LangGeniusVersionResponse = {
current_version: '0.6.0',
latest_version: '0.6.0',
release_notes: 'https://github.com/langgenius/dify/releases/tag/0.6.0',
version: '0.6.0',
release_date: '2024-01-01',
can_auto_update: false,
current_env: 'production',
}
const mockOnCancel = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockIsCEEdition = false
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
systemFeatures: { branding: { enabled: false } },
} as unknown as GlobalPublicStore))
})
describe('Rendering', () => {
it('should render correctly with version information', () => {
// Act
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
// Assert
expect(screen.getByText(/^Version/)).toBeInTheDocument()
expect(screen.getAllByText(/0.6.0/).length).toBeGreaterThan(0)
})
it('should render branding logo if enabled', () => {
// Arrange
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
systemFeatures: { branding: { enabled: true, workspace_logo: 'custom-logo.png' } },
} as unknown as GlobalPublicStore))
// Act
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
// Assert
const img = screen.getByAltText('logo')
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'custom-logo.png')
})
})
describe('Version Logic', () => {
it('should show "Latest Available" when current version equals latest', () => {
// Act
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
// Assert
expect(screen.getByText(/about.latestAvailable/)).toBeInTheDocument()
})
it('should show "Now Available" when current version is behind', () => {
// Arrange
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
// Act
render(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
// Assert
expect(screen.getByText(/about.nowAvailable/)).toBeInTheDocument()
expect(screen.getByText(/about.updateNow/)).toBeInTheDocument()
})
})
describe('Community Edition', () => {
it('should render correctly in Community Edition', () => {
// Arrange
mockIsCEEdition = true
// Act
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
// Assert
expect(screen.getByText(/Open Source License/)).toBeInTheDocument()
})
it('should hide update button in Community Edition when behind version', () => {
// Arrange
mockIsCEEdition = true
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
// Act
render(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
// Assert
expect(screen.queryByText(/about.updateNow/)).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onCancel when close button is clicked', () => {
// Act
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
// Modal uses Headless UI Dialog which renders into a portal, so we need to use document
const closeButton = document.querySelector('div.absolute.cursor-pointer')
if (!closeButton)
throw new Error('Close button not found')
fireEvent.click(closeButton)
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,218 @@
import type { ModalContextState } from '@/context/modal-context'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import { getDocDownloadUrl } from '@/service/common'
import { downloadUrl } from '@/utils/download'
import Toast from '../../base/toast'
import Compliance from './compliance'
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
return {
...actual,
useProviderContext: vi.fn(),
}
})
vi.mock('@/context/modal-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/modal-context')>()
return {
...actual,
useModalContext: vi.fn(),
}
})
vi.mock('@/service/common', () => ({
getDocDownloadUrl: vi.fn(),
}))
vi.mock('@/utils/download', () => ({
downloadUrl: vi.fn(),
}))
describe('Compliance', () => {
const mockSetShowPricingModal = vi.fn()
const mockSetShowAccountSettingModal = vi.fn()
let queryClient: QueryClient
beforeEach(() => {
vi.clearAllMocks()
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.sandbox,
},
})
vi.mocked(useModalContext).mockReturnValue({
setShowPricingModal: mockSetShowPricingModal,
setShowAccountSettingModal: mockSetShowAccountSettingModal,
} as unknown as ModalContextState)
vi.spyOn(Toast, 'notify').mockImplementation(() => ({}))
})
const renderWithQueryClient = (ui: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
)
}
// Wrapper for tests that need the menu open
const openMenuAndRender = () => {
renderWithQueryClient(<Compliance />)
fireEvent.click(screen.getByRole('button'))
}
describe('Rendering', () => {
it('should render compliance menu trigger', () => {
// Act
renderWithQueryClient(<Compliance />)
// Assert
expect(screen.getByText('common.userProfile.compliance')).toBeInTheDocument()
})
it('should show SOC2, ISO, GDPR items when opened', () => {
// Act
openMenuAndRender()
// Assert
expect(screen.getByText('common.compliance.soc2Type1')).toBeInTheDocument()
expect(screen.getByText('common.compliance.soc2Type2')).toBeInTheDocument()
expect(screen.getByText('common.compliance.iso27001')).toBeInTheDocument()
expect(screen.getByText('common.compliance.gdpr')).toBeInTheDocument()
})
})
describe('Plan-based Content', () => {
it('should show Upgrade badge for sandbox plan on restricted docs', () => {
// Act
openMenuAndRender()
// Assert
// SOC2 Type I is restricted for sandbox
expect(screen.getAllByText('billing.upgradeBtn.encourageShort').length).toBeGreaterThan(0)
})
it('should show Download button for plan that allows it', () => {
// Arrange
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.team,
},
})
// Act
openMenuAndRender()
// Assert
expect(screen.getAllByText('common.operation.download').length).toBeGreaterThan(0)
})
})
describe('Actions', () => {
it('should trigger download mutation successfully', async () => {
// Arrange
const mockUrl = 'http://example.com/doc.pdf'
vi.mocked(getDocDownloadUrl).mockResolvedValue({ url: mockUrl })
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.team,
},
})
// Act
openMenuAndRender()
const downloadButtons = screen.getAllByText('common.operation.download')
fireEvent.click(downloadButtons[0])
// Assert
await waitFor(() => {
expect(getDocDownloadUrl).toHaveBeenCalled()
expect(downloadUrl).toHaveBeenCalledWith({ url: mockUrl })
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
type: 'success',
message: 'common.operation.downloadSuccess',
}))
})
})
it('should handle download mutation error', async () => {
// Arrange
vi.mocked(getDocDownloadUrl).mockRejectedValue(new Error('Download failed'))
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.team,
},
})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { })
// Act
openMenuAndRender()
const downloadButtons = screen.getAllByText('common.operation.download')
fireEvent.click(downloadButtons[0])
// Assert
await waitFor(() => {
expect(getDocDownloadUrl).toHaveBeenCalled()
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'common.operation.downloadFailed',
}))
})
expect(consoleSpy).toHaveBeenCalled()
consoleSpy.mockRestore()
})
it('should handle upgrade click on badge for sandbox plan', () => {
// Act
openMenuAndRender()
const upgradeBadges = screen.getAllByText('billing.upgradeBtn.encourageShort')
fireEvent.click(upgradeBadges[0])
// Assert
expect(mockSetShowPricingModal).toHaveBeenCalled()
})
it('should handle upgrade click on badge for non-sandbox plan', () => {
// Arrange
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.professional,
},
})
// Act
openMenuAndRender()
// SOC2 Type II is restricted for professional
const upgradeBadges = screen.getAllByText('billing.upgradeBtn.encourageShort')
fireEvent.click(upgradeBadges[0])
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: ACCOUNT_SETTING_TAB.BILLING,
})
})
})
})

View File

@@ -0,0 +1,340 @@
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import type { AppContextValue } from '@/context/app-context'
import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useLogout } from '@/service/use-common'
import AppSelector from './index'
vi.mock('../account-setting', () => ({
default: () => <div data-testid="account-setting">AccountSetting</div>,
}))
vi.mock('../account-about', () => ({
default: ({ onCancel }: { onCancel: () => void }) => (
<div data-testid="account-about">
Version
<button onClick={onCancel}>Close</button>
</div>
),
}))
vi.mock('@/app/components/header/github-star', () => ({
default: () => <div data-testid="github-star">GithubStar</div>,
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
useLogout: vi.fn(),
}))
vi.mock('@/context/i18n', () => ({
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
}))
// Mock config and env
const { mockConfig, mockEnv } = vi.hoisted(() => ({
mockConfig: {
IS_CLOUD_EDITION: false,
},
mockEnv: {
env: {
NEXT_PUBLIC_SITE_ABOUT: 'show',
},
},
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
IS_DEV: false,
IS_CE_EDITION: false,
}))
vi.mock('@/env', () => mockEnv)
const baseAppContextValue: AppContextValue = {
userProfile: {
id: '1',
name: 'Test User',
email: 'test@example.com',
avatar: '',
avatar_url: 'avatar.png',
is_password_set: false,
},
mutateUserProfile: vi.fn(),
currentWorkspace: {
id: '1',
name: 'Workspace',
plan: '',
status: '',
created_at: 0,
role: 'owner',
providers: [],
trial_credits: 0,
trial_credits_used: 0,
next_credit_reset_date: 0,
},
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceDatasetOperator: false,
mutateCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: {
current_env: 'testing',
current_version: '0.6.0',
latest_version: '0.6.0',
release_date: '',
release_notes: '',
version: '0.6.0',
can_auto_update: false,
},
useSelector: vi.fn(),
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
}
describe('AccountDropdown', () => {
const mockPush = vi.fn()
const mockLogout = vi.fn()
const mockSetShowAccountSettingModal = vi.fn()
const renderWithRouter = (ui: React.ReactElement) => {
const mockRouter = {
push: mockPush,
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
} as unknown as AppRouterInstance
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return render(
<QueryClientProvider client={queryClient}>
<AppRouterContext.Provider value={mockRouter}>
{ui}
</AppRouterContext.Provider>
</QueryClientProvider>,
)
}
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('localStorage', { removeItem: vi.fn() })
mockConfig.IS_CLOUD_EDITION = false
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
vi.mocked(useGlobalPublicStore).mockImplementation((selector?: unknown) => {
const fullState = { systemFeatures: { branding: { enabled: false } }, setSystemFeatures: vi.fn() }
return typeof selector === 'function' ? (selector as (state: typeof fullState) => unknown)(fullState) : fullState
})
vi.mocked(useProviderContext).mockReturnValue({
isEducationAccount: false,
plan: { type: Plan.sandbox },
} as unknown as ProviderContextState)
vi.mocked(useModalContext).mockReturnValue({
setShowAccountSettingModal: mockSetShowAccountSettingModal,
} as unknown as ModalContextState)
vi.mocked(useLogout).mockReturnValue({
mutateAsync: mockLogout,
} as unknown as ReturnType<typeof useLogout>)
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('Rendering', () => {
it('should render user profile correctly', () => {
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('Test User')).toBeInTheDocument()
expect(screen.getByText('test@example.com')).toBeInTheDocument()
})
it('should show EDU badge for education accounts', () => {
// Arrange
vi.mocked(useProviderContext).mockReturnValue({
isEducationAccount: true,
plan: { type: Plan.sandbox },
} as unknown as ProviderContextState)
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('EDU')).toBeInTheDocument()
})
})
describe('Settings and Support', () => {
it('should trigger setShowAccountSettingModal when settings is clicked', () => {
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('common.userProfile.settings'))
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
})
it('should show Compliance in Cloud Edition for workspace owner', () => {
// Arrange
mockConfig.IS_CLOUD_EDITION = true
vi.mocked(useAppContext).mockReturnValue({
...baseAppContextValue,
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
isCurrentWorkspaceOwner: true,
langGeniusVersionInfo: { ...baseAppContextValue.langGeniusVersionInfo, current_version: '0.6.0', latest_version: '0.6.0' },
})
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.compliance')).toBeInTheDocument()
})
})
describe('Actions', () => {
it('should handle logout correctly', async () => {
// Arrange
mockLogout.mockResolvedValue({})
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('common.userProfile.logout'))
// Assert
await waitFor(() => {
expect(mockLogout).toHaveBeenCalled()
expect(localStorage.removeItem).toHaveBeenCalledWith('setup_status')
expect(mockPush).toHaveBeenCalledWith('/signin')
})
})
it('should show About section when about button is clicked and can close it', () => {
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('common.userProfile.about'))
// Assert
expect(screen.getByTestId('account-about')).toBeInTheDocument()
// Act
fireEvent.click(screen.getByText('Close'))
// Assert
expect(screen.queryByTestId('account-about')).not.toBeInTheDocument()
})
})
describe('Branding and Environment', () => {
it('should hide sections when branding is enabled', () => {
// Arrange
vi.mocked(useGlobalPublicStore).mockImplementation((selector?: unknown) => {
const fullState = { systemFeatures: { branding: { enabled: true } }, setSystemFeatures: vi.fn() }
return typeof selector === 'function' ? (selector as (state: typeof fullState) => unknown)(fullState) : fullState
})
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.queryByText('common.userProfile.helpCenter')).not.toBeInTheDocument()
expect(screen.queryByText('common.userProfile.roadmap')).not.toBeInTheDocument()
})
it('should hide About section when NEXT_PUBLIC_SITE_ABOUT is hide', () => {
// Arrange
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'hide'
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.queryByText('common.userProfile.about')).not.toBeInTheDocument()
})
})
describe('Version Indicators', () => {
it('should show orange indicator when version is not latest', () => {
// Arrange
vi.mocked(useAppContext).mockReturnValue({
...baseAppContextValue,
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
langGeniusVersionInfo: {
...baseAppContextValue.langGeniusVersionInfo,
current_version: '0.6.0',
latest_version: '0.7.0',
},
})
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
const indicator = screen.getByTestId('status-indicator')
expect(indicator).toHaveClass('bg-components-badge-status-light-warning-bg')
})
it('should show green indicator when version is latest', () => {
// Arrange
vi.mocked(useAppContext).mockReturnValue({
...baseAppContextValue,
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
langGeniusVersionInfo: {
...baseAppContextValue.langGeniusVersionInfo,
current_version: '0.7.0',
latest_version: '0.7.0',
},
})
// Act
renderWithRouter(<AppSelector />)
fireEvent.click(screen.getByRole('button'))
// Assert
const indicator = screen.getByTestId('status-indicator')
expect(indicator).toHaveClass('bg-components-badge-status-light-success-bg')
})
})
})

View File

@@ -0,0 +1,183 @@
import type { AppContextValue } from '@/context/app-context'
import { fireEvent, render, screen } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import Support from './support'
const { mockZendeskKey } = vi.hoisted(() => ({
mockZendeskKey: { value: 'test-key' },
}))
vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useAppContext: vi.fn(),
}
})
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
return {
...actual,
useProviderContext: vi.fn(),
}
})
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
IS_CE_EDITION: false,
get ZENDESK_WIDGET_KEY() { return mockZendeskKey.value },
}
})
describe('Support', () => {
const mockCloseAccountDropdown = vi.fn()
const baseAppContextValue: AppContextValue = {
userProfile: {
id: '1',
name: 'Test User',
email: 'test@example.com',
avatar: '',
avatar_url: '',
is_password_set: false,
},
mutateUserProfile: vi.fn(),
currentWorkspace: {
id: '1',
name: 'Workspace',
plan: '',
status: '',
created_at: 0,
role: 'owner',
providers: [],
trial_credits: 0,
trial_credits_used: 0,
next_credit_reset_date: 0,
},
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceDatasetOperator: false,
mutateCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: {
current_env: 'testing',
current_version: '0.6.0',
latest_version: '0.6.0',
release_date: '',
release_notes: '',
version: '0.6.0',
can_auto_update: false,
},
useSelector: vi.fn(),
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
}
beforeEach(() => {
vi.clearAllMocks()
window.zE = vi.fn()
mockZendeskKey.value = 'test-key'
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.professional,
},
})
})
describe('Rendering', () => {
it('should render support menu trigger', () => {
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
// Assert
expect(screen.getByText('common.userProfile.support')).toBeInTheDocument()
})
it('should show forum and community links when opened', () => {
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.community')).toBeInTheDocument()
})
})
describe('Plan-based Channels', () => {
it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => {
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
})
it('should hide dedicated support channels for Sandbox plan', () => {
// Arrange
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.sandbox,
},
})
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
expect(screen.queryByText('common.userProfile.emailSupport')).not.toBeInTheDocument()
})
it('should show "Email Support" when ZENDESK_WIDGET_KEY is absent', () => {
// Arrange
mockZendeskKey.value = ''
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
})
})
describe('Interactions and Links', () => {
it('should call toggleZendeskWindow and closeAccountDropdown when "Contact Us" is clicked', () => {
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('common.userProfile.contactUs'))
// Assert
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
expect(mockCloseAccountDropdown).toHaveBeenCalled()
})
it('should have correct forum and community links', () => {
// Act
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
fireEvent.click(screen.getByRole('button'))
// Assert
const forumLink = screen.getByText('common.userProfile.forum').closest('a')
const communityLink = screen.getByText('common.userProfile.community').closest('a')
expect(forumLink).toHaveAttribute('href', 'https://forum.dify.ai/')
expect(communityLink).toHaveAttribute('href', 'https://discord.gg/5AEfbxcd9k')
})
})
})

View File

@@ -0,0 +1,139 @@
import type { ProviderContextState } from '@/context/provider-context'
import type { IWorkspace } from '@/models/common'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ToastContext } from '@/app/components/base/toast'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import { useWorkspacesContext } from '@/context/workspace-context'
import { switchWorkspace } from '@/service/common'
import WorkplaceSelector from './index'
vi.mock('@/context/workspace-context', () => ({
useWorkspacesContext: vi.fn(),
}))
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
return {
...actual,
useProviderContext: vi.fn(),
}
})
vi.mock('@/service/common', () => ({
switchWorkspace: vi.fn(),
}))
describe('WorkplaceSelector', () => {
const mockWorkspaces: IWorkspace[] = [
{ id: '1', name: 'Workspace 1', current: true, plan: 'professional', status: 'normal', created_at: Date.now() },
{ id: '2', name: 'Workspace 2', current: false, plan: 'sandbox', status: 'normal', created_at: Date.now() },
]
const mockNotify = vi.fn()
const mockAssign = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useWorkspacesContext).mockReturnValue({
workspaces: mockWorkspaces,
})
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
isFetchedPlan: true,
isEducationWorkspace: false,
} as ProviderContextState)
vi.stubGlobal('location', { ...window.location, assign: mockAssign })
})
const renderComponent = () => {
return render(
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
<WorkplaceSelector />
</ToastContext.Provider>,
)
}
describe('Rendering', () => {
it('should render current workspace correctly', () => {
// Act
renderComponent()
// Assert
expect(screen.getByText('Workspace 1')).toBeInTheDocument()
expect(screen.getByText('W')).toBeInTheDocument() // First letter icon
})
it('should open menu and display all workspaces when clicked', () => {
// Act
renderComponent()
fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getAllByText('Workspace 1').length).toBeGreaterThan(0)
expect(screen.getByText('Workspace 2')).toBeInTheDocument()
// The real PlanBadge renders uppercase plan name or "pro"
expect(screen.getByText('pro')).toBeInTheDocument()
expect(screen.getByText('sandbox')).toBeInTheDocument()
})
})
describe('Workspace Switching', () => {
it('should switch workspace successfully', async () => {
// Arrange
vi.mocked(switchWorkspace).mockResolvedValue({
result: 'success',
new_tenant: mockWorkspaces[1],
})
// Act
renderComponent()
fireEvent.click(screen.getByRole('button'))
const workspace2 = screen.getByText('Workspace 2')
fireEvent.click(workspace2)
// Assert
expect(switchWorkspace).toHaveBeenCalledWith({
url: '/workspaces/switch',
body: { tenant_id: '2' },
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'success',
message: 'common.actionMsg.modifiedSuccessfully',
})
expect(mockAssign).toHaveBeenCalled()
})
})
it('should not switch to the already current workspace', () => {
// Act
renderComponent()
fireEvent.click(screen.getByRole('button'))
const workspacesInMenu = screen.getAllByText('Workspace 1')
fireEvent.click(workspacesInMenu[workspacesInMenu.length - 1])
// Assert
expect(switchWorkspace).not.toHaveBeenCalled()
})
it('should handle switching error correctly', async () => {
// Arrange
vi.mocked(switchWorkspace).mockRejectedValue(new Error('Failed'))
// Act
renderComponent()
fireEvent.click(screen.getByRole('button'))
const workspace2 = screen.getByText('Workspace 2')
fireEvent.click(workspace2)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
message: 'common.provider.saveFailed',
})
})
})
})
})

View File

@@ -0,0 +1,126 @@
import type { AccountIntegrate } from '@/models/common'
import { render, screen } from '@testing-library/react'
import { useAccountIntegrates } from '@/service/use-common'
import IntegrationsPage from './index'
vi.mock('@/service/use-common', () => ({
useAccountIntegrates: vi.fn(),
}))
describe('IntegrationsPage', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering connected integrations', () => {
it('should render connected integrations when list is provided', () => {
// Arrange
const mockData: AccountIntegrate[] = [
{ provider: 'google', is_bound: true, link: '', created_at: 1678888888 },
{ provider: 'github', is_bound: true, link: '', created_at: 1678888888 },
]
vi.mocked(useAccountIntegrates).mockReturnValue({
data: {
data: mockData,
},
isPending: false,
isError: false,
} as unknown as ReturnType<typeof useAccountIntegrates>)
// Act
render(<IntegrationsPage />)
// Assert
expect(screen.getByText('common.integrations.connected')).toBeInTheDocument()
expect(screen.getByText('common.integrations.google')).toBeInTheDocument()
expect(screen.getByText('common.integrations.github')).toBeInTheDocument()
// Connect link should not be present when bound
expect(screen.queryByText('common.integrations.connect')).not.toBeInTheDocument()
})
})
describe('Unbound integrations', () => {
it('should render connect link for unbound integrations', () => {
// Arrange
const mockData: AccountIntegrate[] = [
{ provider: 'google', is_bound: false, link: 'https://google.com', created_at: 1678888888 },
]
vi.mocked(useAccountIntegrates).mockReturnValue({
data: {
data: mockData,
},
isPending: false,
isError: false,
} as unknown as ReturnType<typeof useAccountIntegrates>)
// Act
render(<IntegrationsPage />)
// Assert
expect(screen.getByText('common.integrations.google')).toBeInTheDocument()
const connectLink = screen.getByText('common.integrations.connect')
expect(connectLink).toBeInTheDocument()
expect(connectLink.closest('a')).toHaveAttribute('href', 'https://google.com')
})
})
describe('Edge cases', () => {
it('should render nothing when no integrations are provided', () => {
// Arrange
vi.mocked(useAccountIntegrates).mockReturnValue({
data: {
data: [],
},
isPending: false,
isError: false,
} as unknown as ReturnType<typeof useAccountIntegrates>)
// Act
render(<IntegrationsPage />)
// Assert
expect(screen.getByText('common.integrations.connected')).toBeInTheDocument()
expect(screen.queryByText('common.integrations.google')).not.toBeInTheDocument()
expect(screen.queryByText('common.integrations.github')).not.toBeInTheDocument()
})
it('should handle unknown providers gracefully', () => {
// Arrange
const mockData = [
{ provider: 'unknown', is_bound: false, link: '', created_at: 1678888888 } as unknown as AccountIntegrate,
]
vi.mocked(useAccountIntegrates).mockReturnValue({
data: {
data: mockData,
},
isPending: false,
isError: false,
} as unknown as ReturnType<typeof useAccountIntegrates>)
// Act
render(<IntegrationsPage />)
// Assert
expect(screen.queryByText('common.integrations.connect')).not.toBeInTheDocument()
})
it('should handle undefined data gracefully', () => {
// Arrange
vi.mocked(useAccountIntegrates).mockReturnValue({
data: undefined,
isPending: false,
isError: false,
} as unknown as ReturnType<typeof useAccountIntegrates>)
// Act
render(<IntegrationsPage />)
// Assert
expect(screen.getByText('common.integrations.connected')).toBeInTheDocument()
expect(screen.queryByText('common.integrations.google')).not.toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1,18 @@
import { render, screen } from '@testing-library/react'
import Empty from './empty'
describe('Empty State', () => {
describe('Rendering', () => {
it('should render title and documentation link', () => {
// Act
render(<Empty />)
// Assert
expect(screen.getByText('common.apiBasedExtension.title')).toBeInTheDocument()
const link = screen.getByText('common.apiBasedExtension.link')
expect(link).toBeInTheDocument()
// The real useDocLink includes the language prefix (defaulting to /en in tests)
expect(link.closest('a')).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/api-extension/api-extension')
})
})
})

View File

@@ -0,0 +1,151 @@
import type { SetStateAction } from 'react'
import type { ModalContextState, ModalState } from '@/context/modal-context'
import type { ApiBasedExtension } from '@/models/common'
import { fireEvent, render, screen } from '@testing-library/react'
import { useModalContext } from '@/context/modal-context'
import { useApiBasedExtensions } from '@/service/use-common'
import ApiBasedExtensionPage from './index'
vi.mock('@/service/use-common', () => ({
useApiBasedExtensions: vi.fn(),
}))
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
}))
describe('ApiBasedExtensionPage', () => {
const mockRefetch = vi.fn<() => void>()
const mockSetShowApiBasedExtensionModal = vi.fn<(value: SetStateAction<ModalState<ApiBasedExtension> | null>) => void>()
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useModalContext).mockReturnValue({
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
} as unknown as ModalContextState)
})
describe('Rendering', () => {
it('should render empty state when no data exists', () => {
// Arrange
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: [],
isPending: false,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
// Act
render(<ApiBasedExtensionPage />)
// Assert
expect(screen.getByText('common.apiBasedExtension.title')).toBeInTheDocument()
})
it('should render list of extensions when data exists', () => {
// Arrange
const mockData = [
{ id: '1', name: 'Extension 1', api_endpoint: 'url1' },
{ id: '2', name: 'Extension 2', api_endpoint: 'url2' },
]
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: mockData,
isPending: false,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
// Act
render(<ApiBasedExtensionPage />)
// Assert
expect(screen.getByText('Extension 1')).toBeInTheDocument()
expect(screen.getByText('url1')).toBeInTheDocument()
expect(screen.getByText('Extension 2')).toBeInTheDocument()
expect(screen.getByText('url2')).toBeInTheDocument()
})
it('should handle loading state', () => {
// Arrange
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: null,
isPending: true,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
// Act
render(<ApiBasedExtensionPage />)
// Assert
expect(screen.queryByText('common.apiBasedExtension.title')).not.toBeInTheDocument()
expect(screen.getByText('common.apiBasedExtension.add')).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should open modal when clicking add button', () => {
// Arrange
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: [],
isPending: false,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
// Act
render(<ApiBasedExtensionPage />)
fireEvent.click(screen.getByText('common.apiBasedExtension.add'))
// Assert
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
payload: {},
}))
})
it('should call refetch when onSaveCallback is executed from the modal', () => {
// Arrange
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: [],
isPending: false,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
// Act
render(<ApiBasedExtensionPage />)
fireEvent.click(screen.getByText('common.apiBasedExtension.add'))
// Trigger callback manually from the mock call
const callArgs = mockSetShowApiBasedExtensionModal.mock.calls[0][0]
if (typeof callArgs === 'object' && callArgs !== null && 'onSaveCallback' in callArgs) {
if (callArgs.onSaveCallback) {
callArgs.onSaveCallback()
// Assert
expect(mockRefetch).toHaveBeenCalled()
}
}
})
it('should call refetch when an item is updated', () => {
// Arrange
const mockData = [{ id: '1', name: 'Extension 1', api_endpoint: 'url1' }]
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: mockData,
isPending: false,
refetch: mockRefetch,
} as unknown as ReturnType<typeof useApiBasedExtensions>)
render(<ApiBasedExtensionPage />)
// Act - Click edit on the rendered item
fireEvent.click(screen.getByText('common.operation.edit'))
// Retrieve the onSaveCallback from the modal call and execute it
const callArgs = mockSetShowApiBasedExtensionModal.mock.calls[0][0]
if (typeof callArgs === 'object' && callArgs !== null && 'onSaveCallback' in callArgs) {
if (callArgs.onSaveCallback)
callArgs.onSaveCallback()
}
// Assert
expect(mockRefetch).toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,190 @@
import type { TFunction } from 'i18next'
import type { ModalContextState } from '@/context/modal-context'
import type { ApiBasedExtension } from '@/models/common'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import * as reactI18next from 'react-i18next'
import { useModalContext } from '@/context/modal-context'
import { deleteApiBasedExtension } from '@/service/common'
import Item from './item'
// Mock dependencies
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
}))
vi.mock('@/service/common', () => ({
deleteApiBasedExtension: vi.fn(),
}))
describe('Item Component', () => {
const mockData: ApiBasedExtension = {
id: '1',
name: 'Test Extension',
api_endpoint: 'https://api.example.com',
api_key: 'test-api-key',
}
const mockOnUpdate = vi.fn()
const mockSetShowApiBasedExtensionModal = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useModalContext).mockReturnValue({
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
} as unknown as ModalContextState)
})
describe('Rendering', () => {
it('should render extension data correctly', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
// Assert
expect(screen.getByText('Test Extension')).toBeInTheDocument()
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
})
it('should render with minimal extension data', () => {
// Arrange
const minimalData: ApiBasedExtension = { id: '2' }
// Act
render(<Item data={minimalData} onUpdate={mockOnUpdate} />)
// Assert
expect(screen.getByText('common.operation.edit')).toBeInTheDocument()
expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
})
})
describe('Modal Interactions', () => {
it('should open edit modal with correct payload when clicking edit button', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByText('common.operation.edit'))
// Assert
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
payload: mockData,
}))
const lastCall = mockSetShowApiBasedExtensionModal.mock.calls[0][0]
if (typeof lastCall === 'object' && lastCall !== null && 'onSaveCallback' in lastCall)
expect(lastCall.onSaveCallback).toBeInstanceOf(Function)
})
it('should execute onUpdate callback when edit modal save callback is invoked', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByText('common.operation.edit'))
// Assert
const modalCallArg = mockSetShowApiBasedExtensionModal.mock.calls[0][0]
if (typeof modalCallArg === 'object' && modalCallArg !== null && 'onSaveCallback' in modalCallArg) {
const onSaveCallback = modalCallArg.onSaveCallback
if (onSaveCallback) {
onSaveCallback()
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
}
}
})
})
describe('Deletion', () => {
it('should show delete confirmation dialog when clicking delete button', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByText('common.operation.delete'))
// Assert
expect(screen.getByText(/common\.operation\.delete.*Test Extension.*\?/i)).toBeInTheDocument()
})
it('should call delete API and triggers onUpdate when confirming deletion', async () => {
// Arrange
vi.mocked(deleteApiBasedExtension).mockResolvedValue({ result: 'success' })
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
// Act
fireEvent.click(screen.getByText('common.operation.delete'))
const dialog = screen.getByTestId('confirm-overlay')
const confirmButton = within(dialog).getByText('common.operation.delete')
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(deleteApiBasedExtension).toHaveBeenCalledWith('/api-based-extension/1')
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
})
})
it('should hide delete confirmation dialog after successful deletion', async () => {
// Arrange
vi.mocked(deleteApiBasedExtension).mockResolvedValue({ result: 'success' })
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
// Act
fireEvent.click(screen.getByText('common.operation.delete'))
const dialog = screen.getByTestId('confirm-overlay')
const confirmButton = within(dialog).getByText('common.operation.delete')
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(screen.queryByText(/common\.operation\.delete.*Test Extension.*\?/i)).not.toBeInTheDocument()
})
})
it('should close delete confirmation when clicking cancel button', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByText('common.operation.delete'))
fireEvent.click(screen.getByText('common.operation.cancel'))
// Assert
expect(screen.queryByText(/common\.operation\.delete.*Test Extension.*\?/i)).not.toBeInTheDocument()
})
it('should not call delete API when canceling deletion', () => {
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByText('common.operation.delete'))
fireEvent.click(screen.getByText('common.operation.cancel'))
// Assert
expect(deleteApiBasedExtension).not.toHaveBeenCalled()
expect(mockOnUpdate).not.toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should still show confirmation modal when operation.delete translation is missing', () => {
// Arrange
const useTranslationSpy = vi.spyOn(reactI18next, 'useTranslation')
const originalValue = useTranslationSpy.getMockImplementation()?.() || {
t: (key: string) => key,
i18n: { language: 'en', changeLanguage: vi.fn() },
}
useTranslationSpy.mockReturnValue({
...originalValue,
t: vi.fn().mockImplementation((key: string) => {
if (key === 'operation.delete')
return ''
return key
}) as unknown as TFunction,
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
// Act
render(<Item data={mockData} onUpdate={mockOnUpdate} />)
const allButtons = screen.getAllByRole('button')
const editBtn = screen.getByText('operation.edit')
const deleteBtn = allButtons.find(btn => btn !== editBtn)
if (deleteBtn)
fireEvent.click(deleteBtn)
// Assert
expect(screen.getByText(/.*Test Extension.*\?/i)).toBeInTheDocument()
useTranslationSpy.mockRestore()
})
})
})

View File

@@ -0,0 +1,223 @@
import type { TFunction } from 'i18next'
import type { IToastProps } from '@/app/components/base/toast'
import { fireEvent, render as RTLRender, screen, waitFor } from '@testing-library/react'
import * as reactI18next from 'react-i18next'
import { ToastContext } from '@/app/components/base/toast'
import { useDocLink } from '@/context/i18n'
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
import ApiBasedExtensionModal from './modal'
vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(),
}))
vi.mock('@/service/common', () => ({
addApiBasedExtension: vi.fn(),
updateApiBasedExtension: vi.fn(),
}))
describe('ApiBasedExtensionModal', () => {
const mockOnCancel = vi.fn()
const mockOnSave = vi.fn()
const mockNotify = vi.fn()
const mockDocLink = vi.fn((path?: string) => `https://docs.dify.ai${path || ''}`)
const render = (ui: React.ReactElement) => RTLRender(
<ToastContext.Provider value={{
notify: mockNotify as unknown as (props: IToastProps) => void,
close: vi.fn(),
}}
>
{ui}
</ToastContext.Provider>,
)
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useDocLink).mockReturnValue(mockDocLink)
})
describe('Rendering', () => {
it('should render correctly for adding a new extension', () => {
// Act
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Assert
expect(screen.getByText('common.apiBasedExtension.modal.title')).toBeInTheDocument()
expect(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder')).toBeInTheDocument()
expect(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder')).toBeInTheDocument()
expect(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder')).toBeInTheDocument()
})
it('should render correctly for editing an existing extension', () => {
// Arrange
const data = { id: '1', name: 'Existing', api_endpoint: 'url', api_key: 'key' }
// Act
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Assert
expect(screen.getByText('common.apiBasedExtension.modal.editTitle')).toBeInTheDocument()
expect(screen.getByDisplayValue('Existing')).toBeInTheDocument()
expect(screen.getByDisplayValue('url')).toBeInTheDocument()
expect(screen.getByDisplayValue('key')).toBeInTheDocument()
})
})
describe('Form Submissions', () => {
it('should call addApiBasedExtension on save for new extension', async () => {
// Arrange
vi.mocked(addApiBasedExtension).mockResolvedValue({ id: 'new-id' })
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Act
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder'), { target: { value: 'https://api.test' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder'), { target: { value: 'secret-key' } })
fireEvent.click(screen.getByText('common.operation.save'))
// Assert
await waitFor(() => {
expect(addApiBasedExtension).toHaveBeenCalledWith({
url: '/api-based-extension',
body: {
name: 'New Ext',
api_endpoint: 'https://api.test',
api_key: 'secret-key',
},
})
expect(mockOnSave).toHaveBeenCalledWith({ id: 'new-id' })
})
})
it('should call updateApiBasedExtension on save for existing extension', async () => {
// Arrange
const data = { id: '1', name: 'Existing', api_endpoint: 'url', api_key: 'long-secret-key' }
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, name: 'Updated' })
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Act
fireEvent.change(screen.getByDisplayValue('Existing'), { target: { value: 'Updated' } })
fireEvent.click(screen.getByText('common.operation.save'))
// Assert
await waitFor(() => {
expect(updateApiBasedExtension).toHaveBeenCalledWith({
url: '/api-based-extension/1',
body: expect.objectContaining({
id: '1',
name: 'Updated',
api_endpoint: 'url',
api_key: '[__HIDDEN__]',
}),
})
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.actionMsg.modifiedSuccessfully' })
expect(mockOnSave).toHaveBeenCalled()
})
})
it('should call updateApiBasedExtension with new api_key when key is changed', async () => {
// Arrange
const data = { id: '1', name: 'Existing', api_endpoint: 'url', api_key: 'old-key' }
vi.mocked(updateApiBasedExtension).mockResolvedValue({ ...data, api_key: 'new-longer-key' })
render(<ApiBasedExtensionModal data={data} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Act
fireEvent.change(screen.getByDisplayValue('old-key'), { target: { value: 'new-longer-key' } })
fireEvent.click(screen.getByText('common.operation.save'))
// Assert
await waitFor(() => {
expect(updateApiBasedExtension).toHaveBeenCalledWith({
url: '/api-based-extension/1',
body: expect.objectContaining({
api_key: 'new-longer-key',
}),
})
})
})
})
describe('Validation', () => {
it('should show error if api key is too short', async () => {
// Arrange
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Act
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'Ext' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder'), { target: { value: 'url' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder'), { target: { value: '123' } })
fireEvent.click(screen.getByText('common.operation.save'))
// Assert
expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'common.apiBasedExtension.modal.apiKey.lengthError' })
expect(addApiBasedExtension).not.toHaveBeenCalled()
})
})
describe('Interactions', () => {
it('should work when onSave is not provided', async () => {
// Arrange
vi.mocked(addApiBasedExtension).mockResolvedValue({ id: 'new-id' })
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
// Act
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.name.placeholder'), { target: { value: 'New Ext' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiEndpoint.placeholder'), { target: { value: 'https://api.test' } })
fireEvent.change(screen.getByPlaceholderText('common.apiBasedExtension.modal.apiKey.placeholder'), { target: { value: 'secret-key' } })
fireEvent.click(screen.getByText('common.operation.save'))
// Assert
await waitFor(() => {
expect(addApiBasedExtension).toHaveBeenCalled()
})
})
it('should call onCancel when clicking cancel button', () => {
// Arrange
render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} onSave={mockOnSave} />)
// Act
fireEvent.click(screen.getByText('common.operation.cancel'))
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should handle missing translations for placeholders gracefully', () => {
// Arrange
const useTranslationSpy = vi.spyOn(reactI18next, 'useTranslation')
const originalValue = useTranslationSpy.getMockImplementation()?.() || {
t: (key: string) => key,
i18n: { language: 'en', changeLanguage: vi.fn() },
}
useTranslationSpy.mockReturnValue({
...originalValue,
t: vi.fn().mockImplementation((key: string) => {
const missingKeys = [
'apiBasedExtension.modal.name.placeholder',
'apiBasedExtension.modal.apiEndpoint.placeholder',
'apiBasedExtension.modal.apiKey.placeholder',
]
if (missingKeys.some(k => key.includes(k)))
return ''
return key
}) as unknown as TFunction,
} as unknown as ReturnType<typeof reactI18next.useTranslation>)
// Act
const { container } = render(<ApiBasedExtensionModal data={{}} onCancel={mockOnCancel} />)
// Assert
const inputs = container.querySelectorAll('input')
inputs.forEach((input) => {
expect(input.placeholder).toBe('')
})
useTranslationSpy.mockRestore()
})
})
})

View File

@@ -0,0 +1,123 @@
import type { UseQueryResult } from '@tanstack/react-query'
import type { ModalContextState } from '@/context/modal-context'
import type { ApiBasedExtension } from '@/models/common'
import { fireEvent, render, screen } from '@testing-library/react'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
import { useApiBasedExtensions } from '@/service/use-common'
import ApiBasedExtensionSelector from './selector'
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
useApiBasedExtensions: vi.fn(),
}))
describe('ApiBasedExtensionSelector', () => {
const mockOnChange = vi.fn()
const mockSetShowAccountSettingModal = vi.fn()
const mockSetShowApiBasedExtensionModal = vi.fn()
const mockRefetch = vi.fn()
const mockData: ApiBasedExtension[] = [
{ id: '1', name: 'Extension 1', api_endpoint: 'https://api1.test' },
{ id: '2', name: 'Extension 2', api_endpoint: 'https://api2.test' },
]
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useModalContext).mockReturnValue({
setShowAccountSettingModal: mockSetShowAccountSettingModal,
setShowApiBasedExtensionModal: mockSetShowApiBasedExtensionModal,
} as unknown as ModalContextState)
vi.mocked(useApiBasedExtensions).mockReturnValue({
data: mockData,
refetch: mockRefetch,
isPending: false,
isError: false,
} as unknown as UseQueryResult<ApiBasedExtension[], Error>)
})
describe('Rendering', () => {
it('should render placeholder when no value is selected', () => {
// Act
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
// Assert
expect(screen.getByText('common.apiBasedExtension.selector.placeholder')).toBeInTheDocument()
})
it('should render selected item name', async () => {
// Act
render(<ApiBasedExtensionSelector value="1" onChange={mockOnChange} />)
// Assert
expect(screen.getByText('Extension 1')).toBeInTheDocument()
})
})
describe('Dropdown Interactions', () => {
it('should open dropdown when clicked', async () => {
// Act
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
const trigger = screen.getByText('common.apiBasedExtension.selector.placeholder')
fireEvent.click(trigger)
// Assert
expect(await screen.findByText('common.apiBasedExtension.selector.title')).toBeInTheDocument()
})
it('should call onChange and closes dropdown when an extension is selected', async () => {
// Act
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
fireEvent.click(screen.getByText('common.apiBasedExtension.selector.placeholder'))
const option = await screen.findByText('Extension 2')
fireEvent.click(option)
// Assert
expect(mockOnChange).toHaveBeenCalledWith('2')
})
})
describe('Manage and Add Extensions', () => {
it('should open account settings when clicking manage', async () => {
// Act
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
fireEvent.click(screen.getByText('common.apiBasedExtension.selector.placeholder'))
const manageButton = await screen.findByText('common.apiBasedExtension.selector.manage')
fireEvent.click(manageButton)
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
})
})
it('should open add modal when clicking add button and refetches on save', async () => {
// Act
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
fireEvent.click(screen.getByText('common.apiBasedExtension.selector.placeholder'))
const addButton = await screen.findByText('common.operation.add')
fireEvent.click(addButton)
// Assert
expect(mockSetShowApiBasedExtensionModal).toHaveBeenCalledWith(expect.objectContaining({
payload: {},
}))
// Trigger callback
const lastCall = mockSetShowApiBasedExtensionModal.mock.calls[0][0]
if (typeof lastCall === 'object' && lastCall !== null && 'onSaveCallback' in lastCall) {
if (lastCall.onSaveCallback) {
lastCall.onSaveCallback()
expect(mockRefetch).toHaveBeenCalled()
}
}
})
})
})

View File

@@ -0,0 +1,121 @@
import type { IItem } from './index'
import { fireEvent, render, screen } from '@testing-library/react'
import Collapse from './index'
describe('Collapse', () => {
const mockItems: IItem[] = [
{ key: '1', name: 'Item 1' },
{ key: '2', name: 'Item 2' },
]
const mockRenderItem = (item: IItem) => (
<div data-testid={`item-${item.key}`}>
{item.name}
</div>
)
const mockOnSelect = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render title and initially closed state', () => {
// Act
const { container } = render(
<Collapse
title="Test Title"
items={mockItems}
renderItem={mockRenderItem}
/>,
)
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
expect(screen.queryByTestId('item-1')).not.toBeInTheDocument()
expect(container.querySelector('svg')).toBeInTheDocument()
})
it('should apply custom wrapperClassName', () => {
// Act
const { container } = render(
<Collapse
title="Test Title"
items={[]}
renderItem={mockRenderItem}
wrapperClassName="custom-class"
/>,
)
// Assert
expect(container.firstChild).toHaveClass('custom-class')
})
})
describe('Interactions', () => {
it('should toggle content open and closed', () => {
// Act & Assert
render(
<Collapse
title="Test Title"
items={mockItems}
renderItem={mockRenderItem}
/>,
)
// Initially closed
expect(screen.queryByTestId('item-1')).not.toBeInTheDocument()
// Click to open
fireEvent.click(screen.getByText('Test Title'))
expect(screen.getByTestId('item-1')).toBeInTheDocument()
expect(screen.getByTestId('item-2')).toBeInTheDocument()
// Click to close
fireEvent.click(screen.getByText('Test Title'))
expect(screen.queryByTestId('item-1')).not.toBeInTheDocument()
})
it('should handle item selection', () => {
// Arrange
render(
<Collapse
title="Test Title"
items={mockItems}
renderItem={mockRenderItem}
onSelect={mockOnSelect}
/>,
)
// Act
fireEvent.click(screen.getByText('Test Title'))
const item1 = screen.getByTestId('item-1')
fireEvent.click(item1)
// Assert
expect(mockOnSelect).toHaveBeenCalledTimes(1)
expect(mockOnSelect).toHaveBeenCalledWith(mockItems[0])
})
it('should not crash when onSelect is undefined and item is clicked', () => {
// Arrange
render(
<Collapse
title="Test Title"
items={mockItems}
renderItem={mockRenderItem}
/>,
)
// Act
fireEvent.click(screen.getByText('Test Title'))
const item1 = screen.getByTestId('item-1')
fireEvent.click(item1)
// Assert
// Should not throw
expect(screen.getByTestId('item-1')).toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1,42 @@
import {
ACCOUNT_SETTING_MODAL_ACTION,
ACCOUNT_SETTING_TAB,
DEFAULT_ACCOUNT_SETTING_TAB,
isValidAccountSettingTab,
} from './constants'
describe('AccountSetting Constants', () => {
it('should have correct ACCOUNT_SETTING_MODAL_ACTION', () => {
expect(ACCOUNT_SETTING_MODAL_ACTION).toBe('showSettings')
})
it('should have correct ACCOUNT_SETTING_TAB values', () => {
expect(ACCOUNT_SETTING_TAB.PROVIDER).toBe('provider')
expect(ACCOUNT_SETTING_TAB.MEMBERS).toBe('members')
expect(ACCOUNT_SETTING_TAB.BILLING).toBe('billing')
expect(ACCOUNT_SETTING_TAB.DATA_SOURCE).toBe('data-source')
expect(ACCOUNT_SETTING_TAB.API_BASED_EXTENSION).toBe('api-based-extension')
expect(ACCOUNT_SETTING_TAB.CUSTOM).toBe('custom')
expect(ACCOUNT_SETTING_TAB.LANGUAGE).toBe('language')
})
it('should have correct DEFAULT_ACCOUNT_SETTING_TAB', () => {
expect(DEFAULT_ACCOUNT_SETTING_TAB).toBe(ACCOUNT_SETTING_TAB.MEMBERS)
})
it('isValidAccountSettingTab should return true for valid tabs', () => {
expect(isValidAccountSettingTab('provider')).toBe(true)
expect(isValidAccountSettingTab('members')).toBe(true)
expect(isValidAccountSettingTab('billing')).toBe(true)
expect(isValidAccountSettingTab('data-source')).toBe(true)
expect(isValidAccountSettingTab('api-based-extension')).toBe(true)
expect(isValidAccountSettingTab('custom')).toBe(true)
expect(isValidAccountSettingTab('language')).toBe(true)
})
it('isValidAccountSettingTab should return false for invalid tabs', () => {
expect(isValidAccountSettingTab(null)).toBe(false)
expect(isValidAccountSettingTab('')).toBe(false)
expect(isValidAccountSettingTab('invalid')).toBe(false)
})
})

View File

@@ -0,0 +1,334 @@
import type { AppContextValue } from '@/context/app-context'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { ACCOUNT_SETTING_TAB } from './constants'
import AccountSetting from './index'
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
return {
...actual,
useProviderContext: vi.fn(),
}
})
vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useAppContext: vi.fn(),
}
})
vi.mock('next/navigation', () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
})),
usePathname: vi.fn(() => '/'),
useParams: vi.fn(() => ({})),
useSearchParams: vi.fn(() => ({ get: vi.fn() })),
}))
vi.mock('@/hooks/use-breakpoints', () => ({
MediaType: {
mobile: 'mobile',
tablet: 'tablet',
pc: 'pc',
},
default: vi.fn(),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useDefaultModel: vi.fn(() => ({ data: null, isLoading: false })),
useUpdateDefaultModel: vi.fn(() => ({ trigger: vi.fn() })),
useUpdateModelList: vi.fn(() => vi.fn()),
useModelList: vi.fn(() => ({ data: [], isLoading: false })),
useSystemDefaultModelAndModelList: vi.fn(() => [null, vi.fn()]),
}))
vi.mock('@/service/use-datasource', () => ({
useGetDataSourceListAuth: vi.fn(() => ({ data: { result: [] } })),
}))
vi.mock('@/service/use-common', () => ({
useApiBasedExtensions: vi.fn(() => ({ data: [], isPending: false })),
useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })),
useProviderContext: vi.fn(),
}))
const baseAppContextValue: AppContextValue = {
userProfile: {
id: '1',
name: 'Test User',
email: 'test@example.com',
avatar: '',
avatar_url: '',
is_password_set: false,
},
mutateUserProfile: vi.fn(),
currentWorkspace: {
id: '1',
name: 'Workspace',
plan: '',
status: '',
created_at: 0,
role: 'owner',
providers: [],
trial_credits: 0,
trial_credits_used: 0,
next_credit_reset_date: 0,
},
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceDatasetOperator: false,
mutateCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: {
current_env: 'testing',
current_version: '0.1.0',
latest_version: '0.1.0',
release_date: '',
release_notes: '',
version: '0.1.0',
can_auto_update: false,
},
useSelector: vi.fn(),
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
}
describe('AccountSetting', () => {
const mockOnCancel = vi.fn()
const mockOnTabChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
enableBilling: true,
enableReplaceWebAppLogo: true,
})
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc)
})
describe('Rendering', () => {
it('should render the sidebar with correct menu items', () => {
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
// Assert
expect(screen.getByText('common.userProfile.settings')).toBeInTheDocument()
expect(screen.getByText('common.settings.provider')).toBeInTheDocument()
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(0)
expect(screen.getByText('common.settings.billing')).toBeInTheDocument()
expect(screen.getByText('common.settings.dataSource')).toBeInTheDocument()
expect(screen.getByText('common.settings.apiBasedExtension')).toBeInTheDocument()
expect(screen.getByText('custom.custom')).toBeInTheDocument()
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(0)
})
it('should respect the activeTab prop', () => {
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} activeTab={ACCOUNT_SETTING_TAB.DATA_SOURCE} />
</QueryClientProvider>,
)
// Assert
// Check that the active item title is Data Source
const titles = screen.getAllByText('common.settings.dataSource')
// One in sidebar, one in header.
expect(titles.length).toBeGreaterThan(1)
})
it('should hide sidebar labels on mobile', () => {
// Arrange
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
// Assert
// On mobile, the labels should not be rendered as per the implementation
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
})
it('should filter items for dataset operator', () => {
// Arrange
vi.mocked(useAppContext).mockReturnValue({
...baseAppContextValue,
isCurrentWorkspaceDatasetOperator: true,
})
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
// Assert
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
expect(screen.queryByText('common.settings.members')).not.toBeInTheDocument()
expect(screen.getByText('common.settings.language')).toBeInTheDocument()
})
it('should hide billing and custom tabs when disabled', () => {
// Arrange
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
enableBilling: false,
enableReplaceWebAppLogo: false,
})
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
// Assert
expect(screen.queryByText('common.settings.billing')).not.toBeInTheDocument()
expect(screen.queryByText('custom.custom')).not.toBeInTheDocument()
})
})
describe('Tab Navigation', () => {
it('should change active tab when clicking on menu item', () => {
// Arrange
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} onTabChange={mockOnTabChange} />
</QueryClientProvider>,
)
// Act
fireEvent.click(screen.getByText('common.settings.provider'))
// Assert
expect(mockOnTabChange).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.PROVIDER)
// Check for content from ModelProviderPage
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
})
it('should navigate through various tabs and show correct details', () => {
// Act & Assert
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
// Billing
fireEvent.click(screen.getByText('common.settings.billing'))
// Billing Page renders plansCommon.plan if data is loaded, or generic text.
// Checking for title in header which is always there
expect(screen.getAllByText('common.settings.billing').length).toBeGreaterThan(1)
// Data Source
fireEvent.click(screen.getByText('common.settings.dataSource'))
expect(screen.getAllByText('common.settings.dataSource').length).toBeGreaterThan(1)
// API Based Extension
fireEvent.click(screen.getByText('common.settings.apiBasedExtension'))
expect(screen.getAllByText('common.settings.apiBasedExtension').length).toBeGreaterThan(1)
// Custom
fireEvent.click(screen.getByText('custom.custom'))
// Custom Page uses 'custom.custom' key as well.
expect(screen.getAllByText('custom.custom').length).toBeGreaterThan(1)
// Language
fireEvent.click(screen.getAllByText('common.settings.language')[0])
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(1)
// Members
fireEvent.click(screen.getAllByText('common.settings.members')[0])
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(1)
})
})
describe('Interactions', () => {
it('should call onCancel when clicking close button', () => {
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0])
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call onCancel when pressing Escape key', () => {
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
fireEvent.keyDown(document, { key: 'Escape' })
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should update search value in provider tab', () => {
// Arrange
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
fireEvent.click(screen.getByText('common.settings.provider'))
// Act
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'test-search' } })
// Assert
expect(input).toHaveValue('test-search')
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
})
it('should handle scroll event in panel', () => {
// Act
render(
<QueryClientProvider client={new QueryClient()}>
<AccountSetting onCancel={mockOnCancel} />
</QueryClientProvider>,
)
const scrollContainer = screen.getByRole('dialog').querySelector('.overflow-y-auto')
// Assert
expect(scrollContainer).toBeInTheDocument()
if (scrollContainer) {
// Scroll down
fireEvent.scroll(scrollContainer, { target: { scrollTop: 100 } })
expect(scrollContainer).toHaveClass('overflow-y-auto')
// Scroll back up
fireEvent.scroll(scrollContainer, { target: { scrollTop: 0 } })
}
})
})
})

View File

@@ -0,0 +1,94 @@
import { fireEvent, render, screen } from '@testing-library/react'
import MenuDialog from './menu-dialog'
describe('MenuDialog', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render children when show is true', () => {
// Act
render(
<MenuDialog show={true} onClose={vi.fn()}>
<div data-testid="dialog-content">Content</div>
</MenuDialog>,
)
// Assert
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
})
it('should not render children when show is false', () => {
// Act
render(
<MenuDialog show={false} onClose={vi.fn()}>
<div data-testid="dialog-content">Content</div>
</MenuDialog>,
)
// Assert
expect(screen.queryByTestId('dialog-content')).not.toBeInTheDocument()
})
it('should apply custom className', () => {
// Act
render(
<MenuDialog show={true} onClose={vi.fn()} className="custom-class">
<div data-testid="dialog-content">Content</div>
</MenuDialog>,
)
// Assert
const panel = screen.getByRole('dialog').querySelector('.custom-class')
expect(panel).toBeInTheDocument()
})
})
describe('Interactions', () => {
it('should call onClose when Escape key is pressed', () => {
// Arrange
const onClose = vi.fn()
render(
<MenuDialog show={true} onClose={onClose}>
<div>Content</div>
</MenuDialog>,
)
// Act
fireEvent.keyDown(document, { key: 'Escape' })
// Assert
expect(onClose).toHaveBeenCalled()
})
it('should not call onClose when a key other than Escape is pressed', () => {
// Arrange
const onClose = vi.fn()
render(
<MenuDialog show={true} onClose={onClose}>
<div>Content</div>
</MenuDialog>,
)
// Act
fireEvent.keyDown(document, { key: 'Enter' })
// Assert
expect(onClose).not.toHaveBeenCalled()
})
it('should not crash when Escape is pressed and onClose is not provided', () => {
// Arrange
render(
<MenuDialog show={true}>
<div data-testid="dialog-content">Content</div>
</MenuDialog>,
)
// Act & Assert
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
})
})
})