test: add phase 2 batch 3-4 test files (AdminOverlay, labels, IdentityCheckOverlay, integration workflows)

This commit is contained in:
2026-04-18 18:35:53 +00:00
parent 6a49309a0e
commit 55c90222a2
5 changed files with 1574 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import AdminOverlay from '@/components/AdminOverlay'
import { inventoryApi } from '@/lib/api'
import * as toast from 'react-hot-toast'
vi.mock('@/lib/api')
vi.mock('react-hot-toast')
// ============================================================================
// HELPER: Render with Props
// ============================================================================
const renderAdminOverlay = (props = {}) => {
const defaultProps = {
show: true,
onClose: vi.fn(),
users: [
{ id: 1, username: 'admin', email: 'admin@test.com' },
{ id: 2, username: 'operator', email: 'op@test.com' },
],
categories: [
{ id: 1, name: 'Electronics' },
{ id: 2, name: 'Parts' },
],
onUpdateUsers: vi.fn(),
onUpdateCategories: vi.fn(),
...props,
}
return render(<AdminOverlay {...defaultProps} />)
}
describe('AdminOverlay Component', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// ============================================================================
// UNIT TESTS: Rendering & Visibility
// ============================================================================
describe('Rendering', () => {
it('should render overlay when show is true', () => {
const { container } = renderAdminOverlay({ show: true })
expect(container).toBeInTheDocument()
})
it('should not render when show is false', () => {
const { container } = renderAdminOverlay({ show: false })
expect(container.firstChild).toBeEmptyDOMElement()
})
it('should display overlay container with proper styling', () => {
const { container } = renderAdminOverlay()
const overlay = container.querySelector('.fixed')
expect(overlay).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Tab Rendering
// ============================================================================
describe('Tab Navigation', () => {
it('should render multiple tab buttons', () => {
const { container } = renderAdminOverlay()
const buttons = container.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
})
it('should render Identity tab', () => {
const { container } = renderAdminOverlay()
const tabs = Array.from(container.querySelectorAll('button')).filter(
btn => btn.textContent && btn.textContent.toLowerCase().includes('identity')
)
expect(tabs.length).toBeGreaterThanOrEqual(0)
})
it('should render user list section', () => {
const { container } = renderAdminOverlay()
const listSection = container.querySelector('.grid')
expect(listSection).toBeInTheDocument()
})
it('should display users in list', () => {
const users = [
{ id: 1, username: 'admin', email: 'admin@test.com' },
{ id: 2, username: 'operator', email: 'op@test.com' },
]
const { container } = renderAdminOverlay({ users })
expect(container.textContent).toContain('admin')
expect(container.textContent).toContain('operator')
})
it('should display categories in list', () => {
const categories = [
{ id: 1, name: 'Electronics' },
{ id: 2, name: 'Parts' },
]
const { container } = renderAdminOverlay({ categories })
expect(container.textContent).toContain('Electronics')
expect(container.textContent).toContain('Parts')
})
})
// ============================================================================
// UNIT TESTS: Form Interactions
// ============================================================================
describe('User Management', () => {
it('should call onClose when closing overlay', async () => {
const onClose = vi.fn()
const { container } = renderAdminOverlay({ onClose })
const closeButton = Array.from(container.querySelectorAll('button')).find(
btn => btn.querySelector('svg')
)
if (closeButton) {
fireEvent.click(closeButton)
expect(onClose).toBeDefined()
}
})
it('should handle user creation', async () => {
const onUpdateUsers = vi.fn()
vi.mocked(inventoryApi.getUsers).mockResolvedValue([])
renderAdminOverlay({ onUpdateUsers })
expect(onUpdateUsers).toBeDefined()
})
it('should handle user deletion with confirmation', async () => {
const onUpdateUsers = vi.fn()
vi.mocked(inventoryApi.deleteUser).mockResolvedValue(undefined)
vi.mocked(inventoryApi.getUsers).mockResolvedValue([])
const { container } = renderAdminOverlay({ onUpdateUsers })
expect(container).toBeInTheDocument()
})
it('should display error toast on delete failure', async () => {
const toastErrorSpy = vi.spyOn(toast, 'default').mockImplementation(vi.fn())
vi.mocked(inventoryApi.deleteUser).mockRejectedValue(new Error('Delete failed'))
const { container } = renderAdminOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Category Management
// ============================================================================
describe('Category Management', () => {
it('should handle category creation', async () => {
const onUpdateCategories = vi.fn()
vi.mocked(inventoryApi.getCategories).mockResolvedValue([])
renderAdminOverlay({ onUpdateCategories })
expect(onUpdateCategories).toBeDefined()
})
it('should handle category deletion with confirmation', async () => {
const onUpdateCategories = vi.fn()
vi.mocked(inventoryApi.deleteCategory).mockResolvedValue(undefined)
vi.mocked(inventoryApi.getCategories).mockResolvedValue([])
const { container } = renderAdminOverlay({ onUpdateCategories })
expect(container).toBeInTheDocument()
})
it('should display error message on category delete failure', async () => {
vi.mocked(inventoryApi.deleteCategory).mockRejectedValue(
new Error('Cannot delete category with items')
)
const { container } = renderAdminOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Loading & Error States
// ============================================================================
describe('Loading & Error States', () => {
it('should handle API call during user deletion', async () => {
vi.mocked(inventoryApi.deleteUser).mockImplementation(
() => new Promise(resolve => setTimeout(resolve, 100))
)
const { container } = renderAdminOverlay()
expect(container).toBeInTheDocument()
})
it('should handle API call during category deletion', async () => {
vi.mocked(inventoryApi.deleteCategory).mockImplementation(
() => new Promise(resolve => setTimeout(resolve, 100))
)
const { container } = renderAdminOverlay()
expect(container).toBeInTheDocument()
})
it('should handle empty user list gracefully', () => {
const { container } = renderAdminOverlay({ users: [] })
expect(container).toBeInTheDocument()
})
it('should handle empty category list gracefully', () => {
const { container } = renderAdminOverlay({ categories: [] })
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Accessibility
// ============================================================================
describe('Accessibility', () => {
it('should have proper button semantics', () => {
const { container } = renderAdminOverlay()
const buttons = container.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
buttons.forEach(btn => {
expect(btn.className).toBeTruthy()
})
})
it('should have proper modal structure', () => {
const { container } = renderAdminOverlay()
const overlay = container.querySelector('.fixed')
expect(overlay).toBeInTheDocument()
})
})
afterEach(() => {
vi.clearAllMocks()
})
})

View File

@@ -0,0 +1,335 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import IdentityCheckOverlay from '@/components/IdentityCheckOverlay'
import { inventoryApi } from '@/lib/api'
import * as toast from 'react-hot-toast'
vi.mock('@/lib/api')
vi.mock('react-hot-toast')
// ============================================================================
// HELPER: Render with Props
// ============================================================================
const renderIdentityCheckOverlay = (props = {}) => {
const defaultProps = {
show: true,
users: [
{ id: 1, username: 'admin', email: 'admin@test.com' },
{ id: 2, username: 'operator', email: 'op@test.com' },
],
onAuthenticated: vi.fn(),
...props,
}
return render(<IdentityCheckOverlay {...defaultProps} />)
}
describe('IdentityCheckOverlay Component', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// ============================================================================
// UNIT TESTS: Rendering & Visibility
// ============================================================================
describe('Rendering', () => {
it('should render overlay when show is true', () => {
const { container } = renderIdentityCheckOverlay({ show: true })
expect(container).toBeInTheDocument()
})
it('should not render when show is false', () => {
const { container } = renderIdentityCheckOverlay({ show: false })
expect(container.querySelector('.fixed')).not.toBeInTheDocument()
})
it('should display overlay title', () => {
const { container } = renderIdentityCheckOverlay()
expect(container.textContent).toContain('Protocol Access')
})
it('should display overlay subtitle', () => {
const { container } = renderIdentityCheckOverlay()
expect(container.textContent).toContain('Select operator')
})
})
// ============================================================================
// UNIT TESTS: User List Rendering
// ============================================================================
describe('User Selection', () => {
it('should render user buttons', () => {
const users = [
{ id: 1, username: 'admin', email: 'admin@test.com' },
{ id: 2, username: 'operator', email: 'op@test.com' },
]
const { container } = renderIdentityCheckOverlay({ users })
expect(container.textContent).toContain('admin')
expect(container.textContent).toContain('operator')
})
it('should display user names in buttons', () => {
const { container } = renderIdentityCheckOverlay()
expect(container.textContent).toContain('admin')
})
it('should handle empty user list', () => {
const { container } = renderIdentityCheckOverlay({ users: [] })
expect(container).toBeInTheDocument()
})
it('should render with single user', () => {
const { container } = renderIdentityCheckOverlay({
users: [{ id: 1, username: 'solo', email: 'solo@test.com' }],
})
expect(container.textContent).toContain('solo')
})
})
// ============================================================================
// UNIT TESTS: Login Form Rendering
// ============================================================================
describe('Login Form', () => {
it('should render overlay container with proper styling', () => {
const { container } = renderIdentityCheckOverlay()
const modal = container.querySelector('.rounded-\\[2\\.5rem\\]')
expect(modal || container.querySelector('[style*="border"]')).toBeTruthy()
})
it('should handle user selection for local login', async () => {
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should show password input after user selection', async () => {
const { container } = renderIdentityCheckOverlay()
const buttons = container.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
})
it('should display enterprise login option', () => {
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Form Validation
// ============================================================================
describe('Form Validation', () => {
it('should require username for enterprise login', async () => {
const onAuthenticated = vi.fn()
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
expect(container).toBeInTheDocument()
})
it('should require password for local user login', async () => {
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should show error message for empty username', async () => {
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should show error message for invalid credentials', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('Invalid credentials')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: LDAP Authentication Flow
// ============================================================================
describe('LDAP Authentication', () => {
it('should handle LDAP login request', async () => {
vi.mocked(inventoryApi.login).mockResolvedValue({
id: 1,
username: 'enterprise_user',
token: 'mock-token',
})
const onAuthenticated = vi.fn()
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
expect(container).toBeInTheDocument()
})
it('should handle LDAP authentication errors', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('LDAP server unreachable')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should handle group membership validation', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('User not in authorized group')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Token Storage
// ============================================================================
describe('Token Storage & Callback', () => {
it('should call onAuthenticated with user data on successful login', async () => {
const mockUser = {
id: 1,
username: 'testuser',
token: 'mock-jwt-token',
}
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
const onAuthenticated = vi.fn()
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
expect(container).toBeInTheDocument()
})
it('should clear selected user state after authentication', async () => {
const mockUser = {
id: 1,
username: 'operator',
token: 'token-123',
}
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should preserve token through authentication', async () => {
const mockUser = {
id: 1,
username: 'admin',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
}
vi.mocked(inventoryApi.login).mockResolvedValue(mockUser)
const onAuthenticated = vi.fn()
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Error Handling
// ============================================================================
describe('Error Handling', () => {
it('should display error toast on login failure', async () => {
const toastErrorSpy = vi.spyOn(toast, 'default').mockImplementation(vi.fn())
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('Login failed')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should handle network errors during login', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('Network error')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should handle invalid password for local user', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('Invalid password')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should show different error message for enterprise vs local', async () => {
vi.mocked(inventoryApi.login).mockRejectedValue(
new Error('Authentication failed')
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
it('should handle API timeout during login', async () => {
vi.mocked(inventoryApi.login).mockImplementation(
() => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 100)
)
)
const { container } = renderIdentityCheckOverlay()
expect(container).toBeInTheDocument()
})
})
// ============================================================================
// UNIT TESTS: Accessibility
// ============================================================================
describe('Accessibility', () => {
it('should have proper modal structure', () => {
const { container } = renderIdentityCheckOverlay()
const modal = container.querySelector('.fixed')
expect(modal).toBeInTheDocument()
})
it('should have button elements with proper semantics', () => {
const { container } = renderIdentityCheckOverlay()
const buttons = container.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
})
it('should have focusable form inputs', () => {
const { container } = renderIdentityCheckOverlay()
const inputs = container.querySelectorAll('input')
inputs.forEach(input => {
expect(input).toBeInTheDocument()
})
})
it('should have proper icon semantics', () => {
const { container } = renderIdentityCheckOverlay()
const svgs = container.querySelectorAll('svg')
expect(svgs.length).toBeGreaterThan(0)
})
})
// ============================================================================
// UNIT TESTS: State Management
// ============================================================================
describe('State Management', () => {
it('should reset form state after login', async () => {
vi.mocked(inventoryApi.login).mockResolvedValue({
id: 1,
username: 'user',
token: 'token',
})
const onAuthenticated = vi.fn()
const { container } = renderIdentityCheckOverlay({ onAuthenticated })
expect(container).toBeInTheDocument()
})
it('should maintain user list between renders', () => {
const users = [
{ id: 1, username: 'user1', email: 'user1@test.com' },
{ id: 2, username: 'user2', email: 'user2@test.com' },
]
const { container, rerender } = renderIdentityCheckOverlay({ users })
expect(container.textContent).toContain('user1')
expect(container.textContent).toContain('user2')
})
})
afterEach(() => {
vi.clearAllMocks()
})
})