// @vitest-environment happy-dom
//
// DynamicTable `extraBulkActions` / `hideBulkDelete`:
// 1. Host actions render in the floating bar once rows are selected and
// receive the selected rows / ids.
// 2. `clearSelection()` empties the selection.
// 3. Changing page drops the selection (classic pagination) so a bulk
// action can never target rows that are no longer visible.
// 4. `hideBulkDelete` removes the built-in "Eliminar".
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => () => {},
}))
const I18N = {
t: (_k: string, o?: { defaultValue?: string; count?: number; total?: number }) => {
if (o?.defaultValue && o.count != null && o.total != null) {
return o.defaultValue.replace('{{count}}', String(o.count)).replace('{{total}}', String(o.total))
}
return o?.defaultValue ?? _k
},
i18n: { language: 'es' },
}
vi.mock('react-i18next', () => ({ useTranslation: () => I18N }))
import { DynamicTable } from '../dynamic-table'
import { makeDefaultGetDynamicColumns } from '../dynamic-columns'
import { ApiProvider, type ApiClient } from '../api-context'
import { useMetadataCache } from '../metadata-cache'
import type { TableMetadata } from '../types'
afterEach(cleanup)
const getDynamicColumns = makeDefaultGetDynamicColumns()
function meta(): TableMetadata {
return {
title: 'Issues',
endpoint: '/data/issue',
columns: [
{ key: 'title', label: 'Title', type: 'text', sortable: true, filterable: false, searchable: true },
],
actions: [],
perPageOptions: [10, 20, 50],
defaultPerPage: 10,
searchPlaceholder: 'Buscar...',
enableCRUDActions: false,
hasActions: false,
}
}
function fakeApi(total: number): ApiClient {
const ok = (data: unknown) => ({ data: { success: true, data, meta: { total } } })
return {
get: vi.fn(async (url: string, cfg?: any) => {
if (url.startsWith('/metadata/table/')) return ok(meta())
if (url.endsWith('/facets')) return ok([])
const p = cfg?.params?.page ?? 1
const size = cfg?.params?.per_page ?? 10
const rows: Array<{ id: number; title: string }> = []
for (let i = (p - 1) * size; i < Math.min(p * size, total); i++) rows.push({ id: i + 1, title: `Issue ${i + 1}` })
return ok(rows)
}),
post: vi.fn(async () => ok(null)),
put: vi.fn(async () => ok(null)),
delete: vi.fn(async () => ok(null)),
}
}
async function selectFirstRow() {
await screen.findAllByText('Issue 1')
const boxes = screen.getAllByRole('checkbox')
fireEvent.click(boxes[1]) // [0] is the select-all header checkbox
}
describe('DynamicTable extraBulkActions', () => {
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
useMetadataCache.getState().setMetadata('issue', meta())
})
it('renders host actions with the selection and lets them clear it', async () => {
const seen: unknown[] = []
render(
(
)}
/>
,
)
expect(screen.queryByText('Aprobar seleccionados')).toBeNull()
await selectFirstRow()
fireEvent.click(await screen.findByText('Aprobar seleccionados'))
expect(seen).toEqual([{ rows: 1, ids: [1] }])
await waitFor(() => expect(screen.queryByText('Aprobar seleccionados')).toBeNull())
})
it('accepts a plain node and keeps the built-in delete by default', async () => {
render(
Extra} />
,
)
await selectFirstRow()
expect(await screen.findByText('Extra')).toBeTruthy()
expect(screen.getByText('Eliminar')).toBeTruthy()
})
it('hideBulkDelete removes the built-in delete', async () => {
render(
Extra} />
,
)
await selectFirstRow()
expect(await screen.findByText('Extra')).toBeTruthy()
expect(screen.queryByText('Eliminar')).toBeNull()
})
it('drops the selection when the page changes', async () => {
render(
Extra} />
,
)
await selectFirstRow()
expect(await screen.findByText('Extra')).toBeTruthy()
fireEvent.click(await screen.findByRole('button', { name: /siguiente|next/i }))
await screen.findAllByText('Issue 11')
await waitFor(() => expect(screen.queryByText('Extra')).toBeNull())
})
})