mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test: header account about, account setting and account dropdown (#32283)
This commit is contained in:
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user