mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 18:05:11 +00:00
test: header account about, account setting and account dropdown (#32283)
This commit is contained in:
131
web/app/components/header/account-about/index.spec.tsx
Normal file
131
web/app/components/header/account-about/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
218
web/app/components/header/account-dropdown/compliance.spec.tsx
Normal file
218
web/app/components/header/account-dropdown/compliance.spec.tsx
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
340
web/app/components/header/account-dropdown/index.spec.tsx
Normal file
340
web/app/components/header/account-dropdown/index.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
183
web/app/components/header/account-dropdown/support.spec.tsx
Normal file
183
web/app/components/header/account-dropdown/support.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
42
web/app/components/header/account-setting/constants.spec.ts
Normal file
42
web/app/components/header/account-setting/constants.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
334
web/app/components/header/account-setting/index.spec.tsx
Normal file
334
web/app/components/header/account-setting/index.spec.tsx
Normal 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 } })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user