mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-06 17:14:28 +00:00
Implement basic tests for webui
This commit is contained in:
186
webui/src/__tests__/App.test.tsx
Normal file
186
webui/src/__tests__/App.test.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import App from '@/App'
|
||||
import { InstancesProvider } from '@/contexts/InstancesContext'
|
||||
import { instancesApi } from '@/lib/api'
|
||||
import { Instance } from '@/types/instance'
|
||||
|
||||
// Mock the API
|
||||
vi.mock('@/lib/api', () => ({
|
||||
instancesApi: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
restart: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
serverApi: {
|
||||
getHelp: vi.fn(),
|
||||
getVersion: vi.fn(),
|
||||
getDevices: vi.fn(),
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock health service to avoid real network calls
|
||||
vi.mock('@/lib/healthService', () => ({
|
||||
healthService: {
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
checkHealth: vi.fn(),
|
||||
},
|
||||
checkHealth: vi.fn(),
|
||||
}))
|
||||
|
||||
function renderApp() {
|
||||
return render(
|
||||
<InstancesProvider>
|
||||
<App />
|
||||
</InstancesProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('App Component - Critical Business Logic Only', () => {
|
||||
const mockInstances: Instance[] = [
|
||||
{ name: 'test-instance-1', running: false, options: { model: 'model1.gguf' } },
|
||||
{ name: 'test-instance-2', running: true, options: { model: 'model2.gguf' } }
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(instancesApi.list).mockResolvedValue(mockInstances)
|
||||
})
|
||||
|
||||
describe('End-to-End Instance Management', () => {
|
||||
it('creates new instance with correct API call and updates UI', async () => {
|
||||
const user = userEvent.setup()
|
||||
const newInstance: Instance = {
|
||||
name: 'new-test-instance',
|
||||
running: false,
|
||||
options: { model: 'new-model.gguf' }
|
||||
}
|
||||
vi.mocked(instancesApi.create).mockResolvedValue(newInstance)
|
||||
|
||||
renderApp()
|
||||
|
||||
// Wait for app to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test-instance-1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Complete create flow: button → form → API call → UI update
|
||||
await user.click(screen.getByText('Create Instance'))
|
||||
|
||||
const nameInput = screen.getByLabelText(/Instance Name/)
|
||||
await user.type(nameInput, 'new-test-instance')
|
||||
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
// Verify correct API call
|
||||
await waitFor(() => {
|
||||
expect(instancesApi.create).toHaveBeenCalledWith('new-test-instance', {
|
||||
auto_restart: true, // Default value
|
||||
})
|
||||
})
|
||||
|
||||
// Verify UI updates with new instance
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('new-test-instance')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('updates existing instance with correct API call', async () => {
|
||||
const user = userEvent.setup()
|
||||
const updatedInstance: Instance = {
|
||||
name: 'test-instance-1',
|
||||
running: false,
|
||||
options: { model: 'updated-model.gguf' }
|
||||
}
|
||||
vi.mocked(instancesApi.update).mockResolvedValue(updatedInstance)
|
||||
|
||||
renderApp()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test-instance-1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Complete edit flow: edit button → form → API call
|
||||
const editButtons = screen.getAllByTitle('Edit instance')
|
||||
await user.click(editButtons[0])
|
||||
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
// Verify correct API call with existing instance data
|
||||
await waitFor(() => {
|
||||
expect(instancesApi.update).toHaveBeenCalledWith('test-instance-1', {
|
||||
model: "model1.gguf", // Pre-filled from existing instance
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('renders instances and provides working interface', async () => {
|
||||
renderApp()
|
||||
|
||||
// Verify the app loads instances and renders them
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test-instance-1')).toBeInTheDocument()
|
||||
expect(screen.getByText('test-instance-2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Instances (2)')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Verify action buttons are present (testing integration, not specific actions)
|
||||
expect(screen.getAllByTitle('Start instance').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByTitle('Stop instance').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByTitle('Edit instance').length).toBe(2)
|
||||
expect(screen.getAllByTitle('Delete instance').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('delete confirmation calls correct API', async () => {
|
||||
const user = userEvent.setup()
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||
vi.mocked(instancesApi.delete).mockResolvedValue(undefined)
|
||||
|
||||
renderApp()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test-instance-1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const deleteButtons = screen.getAllByTitle('Delete instance')
|
||||
await user.click(deleteButtons[0])
|
||||
|
||||
// Verify confirmation and API call
|
||||
expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete instance "test-instance-1"?')
|
||||
await waitFor(() => {
|
||||
expect(instancesApi.delete).toHaveBeenCalledWith('test-instance-1')
|
||||
})
|
||||
|
||||
confirmSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('handles instance loading errors gracefully', async () => {
|
||||
vi.mocked(instancesApi.list).mockRejectedValue(new Error('Failed to load instances'))
|
||||
|
||||
renderApp()
|
||||
|
||||
// App should still render and show error
|
||||
expect(screen.getByText('LlamaCtl Dashboard')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Error loading instances')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows empty state when no instances exist', async () => {
|
||||
vi.mocked(instancesApi.list).mockResolvedValue([])
|
||||
|
||||
renderApp()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No instances found')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user