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:
382
webui/src/components/__tests__/InstanceModal.test.tsx
Normal file
382
webui/src/components/__tests__/InstanceModal.test.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import InstanceModal from '@/components/InstanceModal'
|
||||
import { Instance } from '@/types/instance'
|
||||
|
||||
describe('InstanceModal - Form Logic and Validation', () => {
|
||||
const mockOnSave = vi.fn()
|
||||
const mockOnOpenChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Create Mode', () => {
|
||||
it('validates instance name is required', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Try to submit without name
|
||||
const saveButton = screen.getByTestId('modal-save-button')
|
||||
expect(saveButton).toBeDisabled()
|
||||
|
||||
// Add name, button should be enabled
|
||||
const nameInput = screen.getByLabelText(/Instance Name/)
|
||||
await user.type(nameInput, 'test-instance')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(saveButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('validates instance name format', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
const nameInput = screen.getByLabelText(/Instance Name/)
|
||||
|
||||
// Test invalid characters
|
||||
await user.type(nameInput, 'test instance!')
|
||||
|
||||
expect(screen.getByText(/can only contain letters, numbers, hyphens, and underscores/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('modal-save-button')).toBeDisabled()
|
||||
|
||||
// Clear and test valid name
|
||||
await user.clear(nameInput)
|
||||
await user.type(nameInput, 'test-instance-123')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/can only contain letters, numbers, hyphens, and underscores/)).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('modal-save-button')).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('submits form with correct data structure', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Fill required name
|
||||
await user.type(screen.getByLabelText(/Instance Name/), 'my-instance')
|
||||
|
||||
// Submit form
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('my-instance', {
|
||||
auto_restart: true, // Default value
|
||||
})
|
||||
})
|
||||
|
||||
it('form resets when modal reopens', async () => {
|
||||
const { rerender } = render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Fill form
|
||||
const nameInput = screen.getByLabelText(/Instance Name/)
|
||||
await userEvent.setup().type(nameInput, 'temp-name')
|
||||
|
||||
// Close modal
|
||||
rerender(
|
||||
<InstanceModal
|
||||
open={false}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Reopen modal
|
||||
rerender(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Form should be reset
|
||||
const newNameInput = screen.getByLabelText(/Instance Name/)
|
||||
expect(newNameInput).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edit Mode', () => {
|
||||
const mockInstance: Instance = {
|
||||
name: 'existing-instance',
|
||||
running: false,
|
||||
options: {
|
||||
model: 'test-model.gguf',
|
||||
gpu_layers: 10,
|
||||
auto_restart: false
|
||||
}
|
||||
}
|
||||
|
||||
it('pre-fills form with existing instance data', async () => {
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
instance={mockInstance}
|
||||
/>
|
||||
)
|
||||
|
||||
// Name should be pre-filled and disabled
|
||||
const nameInput = screen.getByDisplayValue('existing-instance')
|
||||
expect(nameInput).toBeDisabled()
|
||||
|
||||
// Other fields should be pre-filled (where visible)
|
||||
// Note: Not all fields are easily testable without more complex setup
|
||||
expect(screen.getByText('Edit Instance')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits update with existing data when no changes made', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
instance={mockInstance}
|
||||
/>
|
||||
)
|
||||
|
||||
// Submit without changes
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('existing-instance', {
|
||||
model: 'test-model.gguf',
|
||||
gpu_layers: 10,
|
||||
auto_restart: false
|
||||
})
|
||||
})
|
||||
|
||||
it('shows correct button text for running vs stopped instances', async () => {
|
||||
const runningInstance: Instance = { ...mockInstance, running: true }
|
||||
|
||||
const { rerender } = render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
instance={mockInstance} // stopped
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('modal-save-button')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
instance={runningInstance} // running
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Update & Restart Instance')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auto Restart Configuration', () => {
|
||||
it('shows restart options when auto restart is enabled', async () => {
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Auto restart should be enabled by default
|
||||
const autoRestartCheckbox = screen.getByLabelText(/Auto Restart/)
|
||||
expect(autoRestartCheckbox).toBeChecked()
|
||||
|
||||
// Restart options should be visible
|
||||
expect(screen.getByLabelText(/Max Restarts/)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/Restart Delay/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides restart options when auto restart is disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Disable auto restart
|
||||
const autoRestartCheckbox = screen.getByLabelText(/Auto Restart/)
|
||||
await user.click(autoRestartCheckbox)
|
||||
|
||||
// Restart options should be hidden
|
||||
expect(screen.queryByLabelText(/Max Restarts/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText(/Restart Delay/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('includes restart options in form submission when enabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Fill form
|
||||
await user.type(screen.getByLabelText(/Instance Name/), 'test-instance')
|
||||
|
||||
// Set restart options
|
||||
await user.type(screen.getByLabelText(/Max Restarts/), '5')
|
||||
await user.type(screen.getByLabelText(/Restart Delay/), '10')
|
||||
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('test-instance', {
|
||||
auto_restart: true,
|
||||
max_restarts: 5,
|
||||
restart_delay: 10
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Advanced Fields Toggle', () => {
|
||||
it('shows advanced fields when toggle clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Advanced fields should be hidden initially
|
||||
expect(screen.queryByText(/Advanced Configuration/)).toBeInTheDocument()
|
||||
|
||||
// Click to expand
|
||||
await user.click(screen.getByText(/Advanced Configuration/))
|
||||
|
||||
// Should show more configuration options
|
||||
// Note: Specific fields depend on zodFormUtils configuration
|
||||
// We're testing the toggle behavior, not specific fields
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Data Handling', () => {
|
||||
it('cleans up undefined values before submission', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
// Fill only required field
|
||||
await user.type(screen.getByLabelText(/Instance Name/), 'clean-instance')
|
||||
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
// Should only include non-empty values
|
||||
expect(mockOnSave).toHaveBeenCalledWith('clean-instance', {
|
||||
auto_restart: true, // Only this default value should be included
|
||||
})
|
||||
})
|
||||
|
||||
it('handles numeric fields correctly', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.type(screen.getByLabelText(/Instance Name/), 'numeric-test')
|
||||
|
||||
// Test GPU layers field (numeric)
|
||||
const gpuLayersInput = screen.getByLabelText(/GPU Layers/)
|
||||
await user.type(gpuLayersInput, '15')
|
||||
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('numeric-test', {
|
||||
auto_restart: true,
|
||||
gpu_layers: 15, // Should be number, not string
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Modal Controls', () => {
|
||||
it('calls onOpenChange when cancel button clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('modal-cancel-button'))
|
||||
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls onOpenChange after successful save', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<InstanceModal
|
||||
open={true}
|
||||
onOpenChange={mockOnOpenChange}
|
||||
onSave={mockOnSave}
|
||||
/>
|
||||
)
|
||||
|
||||
await user.type(screen.getByLabelText(/Instance Name/), 'test')
|
||||
await user.click(screen.getByTestId('modal-save-button'))
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalled()
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user