Files
dify/web/app/components/header/account-setting/api-based-extension-page/item.spec.tsx

191 lines
6.8 KiB
TypeScript

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