frontend/tests/pages/assets.test.ts
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect, vi, beforeEach } from 'vitest'
describe('Assets Index Page', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('filtering', () => {
it('builds query string from filters', () => {
const filters = {
q: 'laptop',
category_id: 'cat-123',
location_id: 'loc-456',
condition_id: undefined,
limit: 24,
offset: 0
}
const params = new URLSearchParams()
if (filters.q) params.set('q', filters.q)
if (filters.category_id) params.set('category_id', filters.category_id)
if (filters.location_id) params.set('location_id', filters.location_id)
if (filters.condition_id) params.set('condition_id', filters.condition_id)
params.set('limit', String(filters.limit))
params.set('offset', String(filters.offset))
const queryString = params.toString()
expect(queryString).toContain('q=laptop')
expect(queryString).toContain('category_id=cat-123')
expect(queryString).toContain('location_id=loc-456')
expect(queryString).toContain('limit=24')
expect(queryString).toContain('offset=0')
expect(queryString).not.toContain('condition_id')
})
it('clears all filters', () => {
const filters = {
q: 'search term',
category_id: 'cat-123',
location_id: 'loc-456',
condition_id: 'cond-789',
offset: 24
}
// Clear filters function
filters.q = ''
filters.category_id = undefined as string | undefined
filters.location_id = undefined as string | undefined
filters.condition_id = undefined as string | undefined
filters.offset = 0
expect(filters.q).toBe('')
expect(filters.category_id).toBeUndefined()
expect(filters.location_id).toBeUndefined()
expect(filters.condition_id).toBeUndefined()
expect(filters.offset).toBe(0)
})
it('debounces search input', async () => {
vi.useFakeTimers()
let searchValue = ''
let filterQ = ''
let filterOffset = 24
const handleSearch = (val: string) => {
searchValue = val
setTimeout(() => {
filterQ = val
filterOffset = 0
}, 300)
}
handleSearch('test')
expect(searchValue).toBe('test')
expect(filterQ).toBe('')
vi.advanceTimersByTime(300)
expect(filterQ).toBe('test')
expect(filterOffset).toBe(0)
vi.useRealTimers()
})
})
describe('pagination', () => {
it('calculates current page from offset', () => {
const testCases = [
{ offset: 0, limit: 24, expected: 1 },
{ offset: 24, limit: 24, expected: 2 },
{ offset: 48, limit: 24, expected: 3 },
{ offset: 0, limit: 10, expected: 1 },
{ offset: 10, limit: 10, expected: 2 }
]
testCases.forEach(({ offset, limit, expected }) => {
const page = Math.floor(offset / limit) + 1
expect(page).toBe(expected)
})
})
it('calculates offset from page number', () => {
const testCases = [
{ page: 1, limit: 24, expected: 0 },
{ page: 2, limit: 24, expected: 24 },
{ page: 3, limit: 24, expected: 48 },
{ page: 1, limit: 10, expected: 0 },
{ page: 5, limit: 10, expected: 40 }
]
testCases.forEach(({ page, limit, expected }) => {
const offset = (page - 1) * limit
expect(offset).toBe(expected)
})
})
it('calculates total pages from total items', () => {
const testCases = [
{ total: 100, limit: 24, expected: 5 },
{ total: 24, limit: 24, expected: 1 },
{ total: 25, limit: 24, expected: 2 },
{ total: 0, limit: 24, expected: 0 },
{ total: 50, limit: 10, expected: 5 }
]
testCases.forEach(({ total, limit, expected }) => {
const totalPages = Math.ceil(total / limit)
expect(totalPages).toBe(expected)
})
})
it('shows correct range in footer', () => {
const page = 2
const limit = 24
const total = 100
const start = ((page - 1) * limit) + 1
const end = Math.min(page * limit, total)
expect(start).toBe(25)
expect(end).toBe(48)
})
})
describe('short ID generation', () => {
it('generates short ID from asset ID', () => {
const getShortId = (assetId: string): string => {
return `ATC-${assetId.slice(0, 4).toUpperCase()}`
}
expect(getShortId('abc123def456')).toBe('ATC-ABC1')
expect(getShortId('xyz789')).toBe('ATC-XYZ7')
expect(getShortId('1234567890')).toBe('ATC-1234')
})
})
describe('asset selection', () => {
it('toggles individual asset selection', () => {
const selectedAssets: string[] = []
const toggleAssetSelection = (assetId: string) => {
const index = selectedAssets.indexOf(assetId)
if (index === -1) {
selectedAssets.push(assetId)
} else {
selectedAssets.splice(index, 1)
}
}
toggleAssetSelection('asset-1')
expect(selectedAssets).toContain('asset-1')
toggleAssetSelection('asset-2')
expect(selectedAssets).toContain('asset-1')
expect(selectedAssets).toContain('asset-2')
toggleAssetSelection('asset-1')
expect(selectedAssets).not.toContain('asset-1')
expect(selectedAssets).toContain('asset-2')
})
it('selects all assets', () => {
const assets = [
{ id: 'asset-1' },
{ id: 'asset-2' },
{ id: 'asset-3' }
]
let selectedAssets: string[] = []
expect(selectedAssets).toEqual([])
selectedAssets = assets.map(a => a.id)
expect(selectedAssets).toEqual(['asset-1', 'asset-2', 'asset-3'])
})
it('deselects all assets', () => {
let selectedAssets = ['asset-1', 'asset-2', 'asset-3']
expect(selectedAssets).toHaveLength(3)
selectedAssets = []
expect(selectedAssets).toEqual([])
})
it('determines if all are selected', () => {
const assets = [{ id: 'asset-1' }, { id: 'asset-2' }]
let selectedAssets = ['asset-1', 'asset-2']
let allSelected = assets.length > 0 && selectedAssets.length === assets.length
expect(allSelected).toBe(true)
selectedAssets = ['asset-1']
allSelected = assets.length > 0 && selectedAssets.length === assets.length
expect(allSelected).toBe(false)
selectedAssets = []
allSelected = assets.length > 0 && selectedAssets.length === assets.length
expect(allSelected).toBe(false)
})
})
describe('filter options mapping', () => {
it('maps categories to select options', () => {
const categories = [
{ id: 'cat-1', name: 'Electronics' },
{ id: 'cat-2', name: 'Books' }
]
const categoryOptions = [
{ label: 'Uncategorized', value: 'uncategorized' },
...categories.map(c => ({ label: c.name, value: c.id }))
]
expect(categoryOptions).toEqual([
{ label: 'Uncategorized', value: 'uncategorized' },
{ label: 'Electronics', value: 'cat-1' },
{ label: 'Books', value: 'cat-2' }
])
})
it('maps locations to hierarchical select options', () => {
const locations = [
{ id: 'home', name: 'Home' },
{ id: 'bedroom', name: 'Bedroom', parent_id: 'home' },
{ id: 'closet', name: 'Closet', parent_id: 'bedroom' }
]
interface LocationTreeNode {
location: { id: string, name: string, parent_id?: string }
children: LocationTreeNode[]
}
const childrenMap = new Map<string | undefined, { id: string, name: string, parent_id?: string }[]>()
locations.forEach((location) => {
const parentId = location.parent_id || undefined
if (!childrenMap.has(parentId)) {
childrenMap.set(parentId, [])
}
childrenMap.get(parentId)!.push(location)
})
const buildTree = (parentId: string | undefined): LocationTreeNode[] => {
const children = childrenMap.get(parentId) || []
return children
.sort((a, b) => a.name.localeCompare(b.name))
.map(location => ({
location,
children: buildTree(location.id)
}))
}
const flattenTree = (nodes: LocationTreeNode[], depth = 0): { label: string, value: string }[] => {
return nodes.flatMap((node) => {
const indent = depth > 0 ? `${'\u00A0'.repeat(depth * 2)}└ ` : ''
return [
{ label: `${indent}${node.location.name}`, value: node.location.id },
...flattenTree(node.children, depth + 1)
]
})
}
const locationOptions = flattenTree(buildTree(undefined))
expect(locationOptions).toEqual([
{ label: 'Home', value: 'home' },
{ label: '\u00A0\u00A0└ Bedroom', value: 'bedroom' },
{ label: '\u00A0\u00A0\u00A0\u00A0└ Closet', value: 'closet' }
])
})
it('maps conditions to select options', () => {
const conditions = [
{ id: 'cond-1', label: 'New' },
{ id: 'cond-2', label: 'Used' }
]
const conditionOptions = conditions.map(c => ({ label: c.label, value: c.id }))
expect(conditionOptions).toEqual([
{ label: 'New', value: 'cond-1' },
{ label: 'Used', value: 'cond-2' }
])
})
it('handles empty arrays gracefully', () => {
const categories: { name: string, id: string }[] = []
const categoryOptions = [
{ label: 'Uncategorized', value: 'uncategorized' },
...(categories?.map(c => ({ label: c.name, value: c.id })) || [])
]
expect(categoryOptions).toEqual([{ label: 'Uncategorized', value: 'uncategorized' }])
})
})
describe('navigation after import', () => {
it('navigates to edit page after successful import', () => {
const mockRouterPush = vi.fn()
const assetId = 'new-asset-123'
const onImported = (id: string) => {
mockRouterPush(`/assets/${id}/edit`)
}
onImported(assetId)
expect(mockRouterPush).toHaveBeenCalledWith('/assets/new-asset-123/edit')
})
})
describe('row actions', () => {
it('navigates when dropdown actions are selected', () => {
const routerPush = vi.fn()
const asset = { id: 'asset-123' }
const items = [[
{ label: 'View', onSelect: () => routerPush(`/assets/${asset.id}`) },
{ label: 'Edit', onSelect: () => routerPush(`/assets/${asset.id}/edit`) }
]]
items[0]![0]!.onSelect()
items[0]![1]!.onSelect()
expect(routerPush).toHaveBeenNthCalledWith(1, '/assets/asset-123')
expect(routerPush).toHaveBeenNthCalledWith(2, '/assets/asset-123/edit')
})
})
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 assets', () => {
const assetsResponse = { assets: [], total: 0 }
expect(assetsResponse.assets.length).toBe(0)
})
it('shows assets table when data is loaded', () => {
const assetsResponse = {
assets: [{ id: 'asset-1', name: 'Test Asset' }],
total: 1
}
expect(assetsResponse.assets.length).toBeGreaterThan(0)
})
})
})