test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-12 10:04:56 +08:00
committed by GitHub
parent 10f85074e8
commit d6b025e91e
195 changed files with 12219 additions and 7840 deletions

View File

@ -0,0 +1,123 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import mocks
import { useGlobalPublicStore } from '@/context/global-public-context'
import { PluginPageContext, PluginPageContextProvider, usePluginPageContext } from '../context'
// Mock dependencies
vi.mock('nuqs', () => ({
useQueryState: vi.fn(() => ['plugins', vi.fn()]),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
}))
vi.mock('../../hooks', () => ({
PLUGIN_PAGE_TABS_MAP: {
plugins: 'plugins',
marketplace: 'discover',
},
usePluginPageTabs: () => [
{ value: 'plugins', text: 'Plugins' },
{ value: 'discover', text: 'Explore Marketplace' },
],
}))
// Helper function to mock useGlobalPublicStore with marketplace setting
const mockGlobalPublicStore = (enableMarketplace: boolean) => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = { systemFeatures: { enable_marketplace: enableMarketplace } }
return selector(state as Parameters<typeof selector>[0])
})
}
// Test component that uses the context
const TestConsumer = () => {
const containerRef = usePluginPageContext(v => v.containerRef)
const options = usePluginPageContext(v => v.options)
const activeTab = usePluginPageContext(v => v.activeTab)
return (
<div>
<span data-testid="has-container-ref">{containerRef ? 'true' : 'false'}</span>
<span data-testid="options-count">{options.length}</span>
<span data-testid="active-tab">{activeTab}</span>
{options.map((opt: { value: string, text: string }) => (
<span key={opt.value} data-testid={`option-${opt.value}`}>{opt.text}</span>
))}
</div>
)
}
describe('PluginPageContext', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('PluginPageContextProvider', () => {
it('should provide context values to children', () => {
mockGlobalPublicStore(true)
render(
<PluginPageContextProvider>
<TestConsumer />
</PluginPageContextProvider>,
)
expect(screen.getByTestId('has-container-ref')).toHaveTextContent('true')
expect(screen.getByTestId('options-count')).toHaveTextContent('2')
})
it('should include marketplace tab when enable_marketplace is true', () => {
mockGlobalPublicStore(true)
render(
<PluginPageContextProvider>
<TestConsumer />
</PluginPageContextProvider>,
)
expect(screen.getByTestId('option-plugins')).toBeInTheDocument()
expect(screen.getByTestId('option-discover')).toBeInTheDocument()
})
it('should filter out marketplace tab when enable_marketplace is false', () => {
mockGlobalPublicStore(false)
render(
<PluginPageContextProvider>
<TestConsumer />
</PluginPageContextProvider>,
)
expect(screen.getByTestId('option-plugins')).toBeInTheDocument()
expect(screen.queryByTestId('option-discover')).not.toBeInTheDocument()
expect(screen.getByTestId('options-count')).toHaveTextContent('1')
})
})
describe('usePluginPageContext', () => {
it('should select specific context values', () => {
mockGlobalPublicStore(true)
render(
<PluginPageContextProvider>
<TestConsumer />
</PluginPageContextProvider>,
)
// activeTab should be 'plugins' from the mock
expect(screen.getByTestId('active-tab')).toHaveTextContent('plugins')
})
})
describe('Default Context Values', () => {
it('should have empty options by default from context', () => {
// Test that the context has proper default values by checking the exported constant
// The PluginPageContext is created with default values including empty options array
expect(PluginPageContext).toBeDefined()
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../../base/modal', () => ({
default: ({ children, title, isShow }: { children: React.ReactNode, title: string, isShow: boolean }) => (
isShow
? (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
</div>
)
: null
),
}))
vi.mock('../../base/key-value-item', () => ({
default: ({ label, value }: { label: string, value: string }) => (
<div data-testid="key-value-item">
<span data-testid="kv-label">{label}</span>
<span data-testid="kv-value">{value}</span>
</div>
),
}))
vi.mock('../../install-plugin/utils', () => ({
convertRepoToUrl: (repo: string) => `https://github.com/${repo}`,
}))
describe('PlugInfo', () => {
let PlugInfo: (typeof import('../plugin-info'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../plugin-info')
PlugInfo = mod.default
})
it('should render modal with title', () => {
render(<PlugInfo onHide={vi.fn()} />)
expect(screen.getByTestId('modal')).toBeInTheDocument()
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.pluginInfoModal.title')
})
it('should display repository info', () => {
render(<PlugInfo repository="org/plugin" onHide={vi.fn()} />)
const kvItems = screen.getAllByTestId('key-value-item')
expect(kvItems.length).toBeGreaterThanOrEqual(1)
const values = screen.getAllByTestId('kv-value')
expect(values.some(v => v.textContent?.includes('https://github.com/org/plugin'))).toBe(true)
})
it('should display release info', () => {
render(<PlugInfo release="v1.0.0" onHide={vi.fn()} />)
const values = screen.getAllByTestId('kv-value')
expect(values.some(v => v.textContent === 'v1.0.0')).toBe(true)
})
it('should display package name', () => {
render(<PlugInfo packageName="my-plugin.difypkg" onHide={vi.fn()} />)
const values = screen.getAllByTestId('kv-value')
expect(values.some(v => v.textContent === 'my-plugin.difypkg')).toBe(true)
})
it('should not show items for undefined props', () => {
render(<PlugInfo onHide={vi.fn()} />)
expect(screen.queryAllByTestId('key-value-item')).toHaveLength(0)
})
})

View File

@ -0,0 +1,381 @@
import { renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import mocks for assertions
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
import Toast from '../../../base/toast'
import { PermissionType } from '../../types'
import useReferenceSetting, { useCanInstallPluginFromMarketplace } from '../use-reference-setting'
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
}))
vi.mock('@/service/use-plugins', () => ({
useReferenceSettings: vi.fn(),
useMutationReferenceSettings: vi.fn(),
useInvalidateReferenceSettings: vi.fn(),
}))
vi.mock('../../../base/toast', () => ({
default: {
notify: vi.fn(),
},
}))
describe('useReferenceSetting Hook', () => {
beforeEach(() => {
vi.clearAllMocks()
// Default mocks
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.everyone,
debug_permission: PermissionType.everyone,
},
},
} as ReturnType<typeof useReferenceSettings>)
vi.mocked(useMutationReferenceSettings).mockReturnValue({
mutate: vi.fn(),
isPending: false,
} as unknown as ReturnType<typeof useMutationReferenceSettings>)
vi.mocked(useInvalidateReferenceSettings).mockReturnValue(vi.fn())
})
describe('hasPermission logic', () => {
it('should return false when permission is undefined', () => {
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: undefined,
debug_permission: undefined,
},
},
} as unknown as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(false)
expect(result.current.canDebugger).toBe(false)
})
it('should return false when permission is noOne', () => {
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(false)
expect(result.current.canDebugger).toBe(false)
})
it('should return true when permission is everyone', () => {
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.everyone,
debug_permission: PermissionType.everyone,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(true)
expect(result.current.canDebugger).toBe(true)
})
it('should return isAdmin when permission is admin and user is manager', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.admin,
debug_permission: PermissionType.admin,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(true)
expect(result.current.canDebugger).toBe(true)
})
it('should return isAdmin when permission is admin and user is owner', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: true,
} as ReturnType<typeof useAppContext>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.admin,
debug_permission: PermissionType.admin,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(true)
expect(result.current.canDebugger).toBe(true)
})
it('should return false when permission is admin and user is not admin', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.admin,
debug_permission: PermissionType.admin,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(false)
expect(result.current.canDebugger).toBe(false)
})
})
describe('canSetPermissions', () => {
it('should be true when user is workspace manager', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canSetPermissions).toBe(true)
})
it('should be true when user is workspace owner', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: true,
} as ReturnType<typeof useAppContext>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canSetPermissions).toBe(true)
})
it('should be false when user is neither manager nor owner', () => {
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canSetPermissions).toBe(false)
})
})
describe('setReferenceSettings callback', () => {
it('should call invalidateReferenceSettings and show toast on success', async () => {
const mockInvalidate = vi.fn()
vi.mocked(useInvalidateReferenceSettings).mockReturnValue(mockInvalidate)
let onSuccessCallback: (() => void) | undefined
vi.mocked(useMutationReferenceSettings).mockImplementation((options) => {
onSuccessCallback = options?.onSuccess as () => void
return {
mutate: vi.fn(),
isPending: false,
} as unknown as ReturnType<typeof useMutationReferenceSettings>
})
renderHook(() => useReferenceSetting())
// Trigger the onSuccess callback
if (onSuccessCallback)
onSuccessCallback()
await waitFor(() => {
expect(mockInvalidate).toHaveBeenCalled()
expect(Toast.notify).toHaveBeenCalledWith({
type: 'success',
message: 'common.api.actionSuccess',
})
})
})
})
describe('returned values', () => {
it('should return referenceSetting data', () => {
const mockData = {
permission: {
install_permission: PermissionType.everyone,
debug_permission: PermissionType.everyone,
},
}
vi.mocked(useReferenceSettings).mockReturnValue({
data: mockData,
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.referenceSetting).toEqual(mockData)
})
it('should return isUpdatePending from mutation', () => {
vi.mocked(useMutationReferenceSettings).mockReturnValue({
mutate: vi.fn(),
isPending: true,
} as unknown as ReturnType<typeof useMutationReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.isUpdatePending).toBe(true)
})
it('should handle null data', () => {
vi.mocked(useReferenceSettings).mockReturnValue({
data: null,
} as unknown as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useReferenceSetting())
expect(result.current.canManagement).toBe(false)
expect(result.current.canDebugger).toBe(false)
})
})
})
describe('useCanInstallPluginFromMarketplace Hook', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useAppContext).mockReturnValue({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
} as ReturnType<typeof useAppContext>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.everyone,
debug_permission: PermissionType.everyone,
},
},
} as ReturnType<typeof useReferenceSettings>)
vi.mocked(useMutationReferenceSettings).mockReturnValue({
mutate: vi.fn(),
isPending: false,
} as unknown as ReturnType<typeof useMutationReferenceSettings>)
vi.mocked(useInvalidateReferenceSettings).mockReturnValue(vi.fn())
})
it('should return true when marketplace is enabled and canManagement is true', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: true,
},
}
return selector(state as Parameters<typeof selector>[0])
})
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
expect(result.current.canInstallPluginFromMarketplace).toBe(true)
})
it('should return false when marketplace is disabled', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: false,
},
}
return selector(state as Parameters<typeof selector>[0])
})
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
})
it('should return false when canManagement is false', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: true,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
})
it('should return false when both marketplace is disabled and canManagement is false', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: false,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
permission: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
},
} as ReturnType<typeof useReferenceSettings>)
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
})
})

View File

@ -0,0 +1,487 @@
import type { RefObject } from 'react'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useUploader } from '../use-uploader'
describe('useUploader Hook', () => {
let mockContainerRef: RefObject<HTMLDivElement | null>
let mockOnFileChange: (file: File | null) => void
let mockContainer: HTMLDivElement
beforeEach(() => {
vi.clearAllMocks()
mockContainer = document.createElement('div')
document.body.appendChild(mockContainer)
mockContainerRef = { current: mockContainer }
mockOnFileChange = vi.fn()
})
afterEach(() => {
if (mockContainer.parentNode)
document.body.removeChild(mockContainer)
})
describe('Initial State', () => {
it('should return initial state with dragging false', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
expect(result.current.dragging).toBe(false)
expect(result.current.fileUploader.current).toBeNull()
expect(result.current.fileChangeHandle).not.toBeNull()
expect(result.current.removeFile).not.toBeNull()
})
it('should return null handlers when disabled', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
enabled: false,
}),
)
expect(result.current.dragging).toBe(false)
expect(result.current.fileChangeHandle).toBeNull()
expect(result.current.removeFile).toBeNull()
})
})
describe('Drag Events', () => {
it('should handle dragenter and set dragging to true', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
expect(result.current.dragging).toBe(true)
})
it('should not set dragging when dragenter without Files type', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['text/plain'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
expect(result.current.dragging).toBe(false)
})
it('should handle dragover event', () => {
renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true })
act(() => {
mockContainer.dispatchEvent(dragOverEvent)
})
// dragover should prevent default and stop propagation
expect(mockContainer).toBeInTheDocument()
})
it('should handle dragleave when relatedTarget is null', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// First set dragging to true
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
expect(result.current.dragging).toBe(true)
// Then trigger dragleave with null relatedTarget
const dragLeaveEvent = new Event('dragleave', { bubbles: true, cancelable: true })
Object.defineProperty(dragLeaveEvent, 'relatedTarget', {
value: null,
})
act(() => {
mockContainer.dispatchEvent(dragLeaveEvent)
})
expect(result.current.dragging).toBe(false)
})
it('should handle dragleave when relatedTarget is outside container', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// First set dragging to true
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
expect(result.current.dragging).toBe(true)
// Create element outside container
const outsideElement = document.createElement('div')
document.body.appendChild(outsideElement)
// Trigger dragleave with relatedTarget outside container
const dragLeaveEvent = new Event('dragleave', { bubbles: true, cancelable: true })
Object.defineProperty(dragLeaveEvent, 'relatedTarget', {
value: outsideElement,
})
act(() => {
mockContainer.dispatchEvent(dragLeaveEvent)
})
expect(result.current.dragging).toBe(false)
document.body.removeChild(outsideElement)
})
it('should not set dragging to false when relatedTarget is inside container', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// First set dragging to true
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
expect(result.current.dragging).toBe(true)
// Create element inside container
const insideElement = document.createElement('div')
mockContainer.appendChild(insideElement)
// Trigger dragleave with relatedTarget inside container
const dragLeaveEvent = new Event('dragleave', { bubbles: true, cancelable: true })
Object.defineProperty(dragLeaveEvent, 'relatedTarget', {
value: insideElement,
})
act(() => {
mockContainer.dispatchEvent(dragLeaveEvent)
})
// Should still be dragging since relatedTarget is inside container
expect(result.current.dragging).toBe(true)
})
})
describe('Drop Events', () => {
it('should handle drop event with files', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// First set dragging to true
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
// Create mock file
const file = new File(['content'], 'test.difypkg', { type: 'application/octet-stream' })
// Trigger drop event
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
Object.defineProperty(dropEvent, 'dataTransfer', {
value: { files: [file] },
})
act(() => {
mockContainer.dispatchEvent(dropEvent)
})
expect(result.current.dragging).toBe(false)
expect(mockOnFileChange).toHaveBeenCalledWith(file)
})
it('should not call onFileChange when drop has no dataTransfer', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// Set dragging first
const dragEnterEvent = new Event('dragenter', { bubbles: true, cancelable: true })
Object.defineProperty(dragEnterEvent, 'dataTransfer', {
value: { types: ['Files'] },
})
act(() => {
mockContainer.dispatchEvent(dragEnterEvent)
})
// Drop without dataTransfer
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
// No dataTransfer property
act(() => {
mockContainer.dispatchEvent(dropEvent)
})
expect(result.current.dragging).toBe(false)
expect(mockOnFileChange).not.toHaveBeenCalled()
})
it('should not call onFileChange when drop has empty files array', () => {
renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
Object.defineProperty(dropEvent, 'dataTransfer', {
value: { files: [] },
})
act(() => {
mockContainer.dispatchEvent(dropEvent)
})
expect(mockOnFileChange).not.toHaveBeenCalled()
})
})
describe('File Change Handler', () => {
it('should call onFileChange with file from input', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const file = new File(['content'], 'test.difypkg', { type: 'application/octet-stream' })
const mockEvent = {
target: {
files: [file],
},
} as unknown as React.ChangeEvent<HTMLInputElement>
act(() => {
result.current.fileChangeHandle?.(mockEvent)
})
expect(mockOnFileChange).toHaveBeenCalledWith(file)
})
it('should call onFileChange with null when no files', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
const mockEvent = {
target: {
files: null,
},
} as unknown as React.ChangeEvent<HTMLInputElement>
act(() => {
result.current.fileChangeHandle?.(mockEvent)
})
expect(mockOnFileChange).toHaveBeenCalledWith(null)
})
})
describe('Remove File', () => {
it('should call onFileChange with null', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
act(() => {
result.current.removeFile?.()
})
expect(mockOnFileChange).toHaveBeenCalledWith(null)
})
it('should handle removeFile when fileUploader has a value', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// Create a mock input element with value property
const mockInput = {
value: 'test.difypkg',
}
// Override the fileUploader ref
Object.defineProperty(result.current.fileUploader, 'current', {
value: mockInput,
writable: true,
})
act(() => {
result.current.removeFile?.()
})
expect(mockOnFileChange).toHaveBeenCalledWith(null)
expect(mockInput.value).toBe('')
})
it('should handle removeFile when fileUploader is null', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
}),
)
// fileUploader.current is null by default
act(() => {
result.current.removeFile?.()
})
expect(mockOnFileChange).toHaveBeenCalledWith(null)
})
})
describe('Enabled/Disabled State', () => {
it('should not add event listeners when disabled', () => {
const addEventListenerSpy = vi.spyOn(mockContainer, 'addEventListener')
renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
enabled: false,
}),
)
expect(addEventListenerSpy).not.toHaveBeenCalled()
})
it('should add event listeners when enabled', () => {
const addEventListenerSpy = vi.spyOn(mockContainer, 'addEventListener')
renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
enabled: true,
}),
)
expect(addEventListenerSpy).toHaveBeenCalledWith('dragenter', expect.any(Function))
expect(addEventListenerSpy).toHaveBeenCalledWith('dragover', expect.any(Function))
expect(addEventListenerSpy).toHaveBeenCalledWith('dragleave', expect.any(Function))
expect(addEventListenerSpy).toHaveBeenCalledWith('drop', expect.any(Function))
})
it('should remove event listeners on cleanup', () => {
const removeEventListenerSpy = vi.spyOn(mockContainer, 'removeEventListener')
const { unmount } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
enabled: true,
}),
)
unmount()
expect(removeEventListenerSpy).toHaveBeenCalledWith('dragenter', expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith('dragover', expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith('dragleave', expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith('drop', expect.any(Function))
})
it('should return false for dragging when disabled', () => {
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: mockContainerRef,
enabled: false,
}),
)
expect(result.current.dragging).toBe(false)
})
})
describe('Container Ref Edge Cases', () => {
it('should handle null containerRef.current', () => {
const nullRef: RefObject<HTMLDivElement | null> = { current: null }
const { result } = renderHook(() =>
useUploader({
onFileChange: mockOnFileChange,
containerRef: nullRef,
}),
)
expect(result.current.dragging).toBe(false)
})
})
})