frontend/tests/pages/categories.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect, vi, beforeEach } from 'vitest'
describe('Categories Index Page', () => {
const mockApiFetch = vi.fn()
const mockToast = { add: vi.fn() }
const mockRefresh = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('statistics', () => {
it('calculates total categories count', () => {
const categories = [
{ id: 'cat-1', name: 'Electronics' },
{ id: 'cat-2', name: 'Books' },
{ id: 'cat-3', name: 'Furniture' }
]
const totalCategories = categories.length
expect(totalCategories).toBe(3)
})
it('calculates total items tracked', () => {
const categoryAssetCounts = {
'cat-1': 10,
'cat-2': 25,
'cat-3': 5
}
const totalItems = Object.values(categoryAssetCounts).reduce((sum, count) => sum + count, 0)
expect(totalItems).toBe(40)
})
it('counts unique fields across categories', () => {
const categories = [
{ id: 'cat-1', attributes: [{ attribute_id: 'a1' }, { attribute_id: 'a2' }] },
{ id: 'cat-2', attributes: [{ attribute_id: 'a2' }, { attribute_id: 'a3' }] },
{ id: 'cat-3', attributes: [] }
]
const fieldIds = new Set(
categories.flatMap(category => category.attributes.map(attribute => attribute.attribute_id))
)
expect(fieldIds.size).toBe(3)
})
it('handles empty categories array', () => {
const categories: { attributes?: unknown[] }[] = []
const totalCategories = categories.length || 0
const uniqueFields = new Set(
categories.flatMap(category => category.attributes || [])
).size
expect(totalCategories).toBe(0)
expect(uniqueFields).toBe(0)
})
})
describe('category styling', () => {
const getCategoryStyle = (category: { name: string, icon?: string }) => {
if (category.icon) {
return { icon: category.icon, bgColor: 'bg-attic-100', textColor: 'text-attic-600' }
}
const name = category.name.toLowerCase()
if (name.includes('electronic') || name.includes('computer')) {
return { icon: 'i-lucide-laptop', bgColor: 'bg-orange-100', textColor: 'text-orange-600' }
}
if (name.includes('book')) {
return { icon: 'i-lucide-book-open', bgColor: 'bg-blue-100', textColor: 'text-blue-600' }
}
if (name.includes('movie') || name.includes('dvd')) {
return { icon: 'i-lucide-film', bgColor: 'bg-purple-100', textColor: 'text-purple-600' }
}
if (name.includes('furniture')) {
return { icon: 'i-lucide-armchair', bgColor: 'bg-amber-100', textColor: 'text-amber-600' }
}
return { icon: 'i-lucide-tag', bgColor: 'bg-attic-100', textColor: 'text-attic-600' }
}
it('returns electronics style for electronics category', () => {
const style = getCategoryStyle({ name: 'Electronics' })
expect(style.icon).toBe('i-lucide-laptop')
expect(style.bgColor).toContain('orange')
})
it('returns book style for books category', () => {
const style = getCategoryStyle({ name: 'Books & Media' })
expect(style.icon).toBe('i-lucide-book-open')
expect(style.bgColor).toContain('blue')
})
it('returns movie style for DVD category', () => {
const style = getCategoryStyle({ name: 'DVD Collection' })
expect(style.icon).toBe('i-lucide-film')
expect(style.bgColor).toContain('purple')
})
it('uses custom icon when provided', () => {
const style = getCategoryStyle({ name: 'Custom', icon: 'i-lucide-star' })
expect(style.icon).toBe('i-lucide-star')
})
it('returns default style for unknown categories', () => {
const style = getCategoryStyle({ name: 'Something Else' })
expect(style.icon).toBe('i-lucide-tag')
})
})
describe('attribute styling', () => {
const getAttributeStyle = (dataType: string) => {
switch (dataType) {
case 'string':
return { icon: 'i-lucide-type', bgColor: 'bg-blue-50', textColor: 'text-blue-600' }
case 'number':
return { icon: 'i-lucide-hash', bgColor: 'bg-amber-50', textColor: 'text-amber-600' }
case 'boolean':
return { icon: 'i-lucide-toggle-left', bgColor: 'bg-green-50', textColor: 'text-green-600' }
case 'text':
return { icon: 'i-lucide-align-left', bgColor: 'bg-purple-50', textColor: 'text-purple-600' }
case 'date':
return { icon: 'i-lucide-calendar', bgColor: 'bg-red-50', textColor: 'text-red-600' }
default:
return { icon: 'i-lucide-circle', bgColor: 'bg-gray-50', textColor: 'text-gray-600' }
}
}
it('returns correct style for string type', () => {
const style = getAttributeStyle('string')
expect(style.icon).toBe('i-lucide-type')
})
it('returns correct style for number type', () => {
const style = getAttributeStyle('number')
expect(style.icon).toBe('i-lucide-hash')
})
it('returns correct style for boolean type', () => {
const style = getAttributeStyle('boolean')
expect(style.icon).toBe('i-lucide-toggle-left')
})
it('returns correct style for date type', () => {
const style = getAttributeStyle('date')
expect(style.icon).toBe('i-lucide-calendar')
})
it('returns default style for unknown type', () => {
const style = getAttributeStyle('unknown')
expect(style.icon).toBe('i-lucide-circle')
})
})
describe('delete category', () => {
it('opens delete confirmation modal', () => {
let deleteModalOpen = false
let categoryToDelete: { id: string, name: string } | null = null
const confirmDelete = (category: { id: string, name: string }) => {
categoryToDelete = category
deleteModalOpen = true
}
const category = { id: 'cat-1', name: 'Electronics' }
confirmDelete(category)
expect(deleteModalOpen).toBe(true)
expect(categoryToDelete).toEqual(category)
})
it('deletes category on confirmation', async () => {
mockApiFetch.mockResolvedValueOnce({})
await mockApiFetch('/api/categories/cat-1', { method: 'DELETE' })
expect(mockApiFetch).toHaveBeenCalledWith('/api/categories/cat-1', { method: 'DELETE' })
})
it('shows success toast after deletion', async () => {
mockApiFetch.mockResolvedValueOnce({})
let deleteModalOpen = true
expect(deleteModalOpen).toBe(true)
let categoryToDelete: { id: string, name: string } | null = { id: 'cat-1', name: 'Test' }
await mockApiFetch(`/api/categories/${categoryToDelete.id}`, { method: 'DELETE' })
mockToast.add({ title: 'Category deleted', color: 'success' })
deleteModalOpen = false
categoryToDelete = null
mockRefresh()
expect(mockToast.add).toHaveBeenCalledWith({ title: 'Category deleted', color: 'success' })
expect(deleteModalOpen).toBe(false)
expect(categoryToDelete).toBeNull()
expect(mockRefresh).toHaveBeenCalled()
})
it('shows error toast on deletion failure', async () => {
mockApiFetch.mockRejectedValueOnce(new Error('Delete failed'))
try {
await mockApiFetch('/api/categories/cat-1', { method: 'DELETE' })
} catch {
mockToast.add({ title: 'Failed to delete category', color: 'error' })
}
expect(mockToast.add).toHaveBeenCalledWith({ title: 'Failed to delete category', color: 'error' })
})
})
describe('view attributes', () => {
it('fetches full category details', async () => {
const fullCategory = {
id: 'cat-1',
name: 'Electronics',
attributes: [
{ id: 'attr-1', attribute: { name: 'Serial Number', data_type: 'string' }, required: true }
]
}
mockApiFetch.mockResolvedValueOnce(fullCategory)
const result = await mockApiFetch('/api/categories/cat-1')
expect(result).toEqual(fullCategory)
})
it('opens attributes modal on success', async () => {
let attributesModalOpen = false
expect(attributesModalOpen).toBe(false)
let viewingCategory: { id: string, name: string, attributes: unknown[] } | null = null
expect(viewingCategory).toBeNull()
const fullCategory = { id: 'cat-1', name: 'Electronics', attributes: [] }
mockApiFetch.mockResolvedValueOnce(fullCategory)
const result = await mockApiFetch('/api/categories/cat-1')
viewingCategory = result
attributesModalOpen = true
expect(viewingCategory).toEqual(fullCategory)
expect(attributesModalOpen).toBe(true)
})
it('shows error toast on fetch failure', async () => {
mockApiFetch.mockRejectedValueOnce(new Error('Fetch failed'))
try {
await mockApiFetch('/api/categories/cat-1')
} catch {
mockToast.add({ title: 'Failed to load category attributes', color: 'error' })
}
expect(mockToast.add).toHaveBeenCalledWith({
title: 'Failed to load category attributes',
color: 'error'
})
})
})
describe('asset count per category', () => {
it('gets asset count for category', () => {
const categoryAssetCounts: Record<string, number> = {
'cat-1': 15,
'cat-2': 8
}
const getAssetCount = (categoryId: string): number => {
return categoryAssetCounts[categoryId] || 0
}
expect(getAssetCount('cat-1')).toBe(15)
expect(getAssetCount('cat-2')).toBe(8)
expect(getAssetCount('cat-3')).toBe(0)
})
})
describe('loading and empty states', () => {
it('shows loading state when status is pending', () => {
const status = 'pending'
expect(status).toBe('pending')
})
it('shows empty state when no categories', () => {
const categories: { id: string, name: string }[] = []
expect(categories.length).toBe(0)
})
it('shows table when categories exist', () => {
const categories = [{ id: 'cat-1', name: 'Test' }]
expect(categories.length).toBeGreaterThan(0)
})
})
})