// @vitest-environment happy-dom // // DynamicTable `defaultSort` + bulk ctx `actions` / `can`: // 1. `defaultSort` seeds the first request (sortBy/order); `desc` omitted // means asc; without it nothing is sent; a `?sortBy=` in the URL wins when // enableUrlSync is on. (Changing it from the header menu is plain // TanStack sorting state and is not exercised here: the Radix menu does // not open reliably under happy-dom.) // 2. The bulk context handed to `extraBulkActions` exposes `actions` (the // metadata actions) and `can(key)`: all allowed without a provider, // filtered by capability inside a PermissionsProvider. 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 { PermissionsProvider } from '../permissions-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: [ { key: 'approve', name: 'approve', label: 'Aprobar', icon: 'Check', class: '', type: 'custom' }, { key: 'reject', name: 'reject', label: 'Rechazar', icon: 'X', class: '', type: 'custom' }, ], perPageOptions: [10, 20, 50], defaultPerPage: 10, searchPlaceholder: 'Buscar...', enableCRUDActions: false, hasActions: true, } } 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 defaultSort', () => { beforeEach(() => { localStorage.clear(); sessionStorage.clear() window.history.replaceState({}, '', '/') useMetadataCache.getState().setMetadata('issue', meta()) }) it('sends the default sort on the first request', async () => { const api = fakeApi(5) render( , ) await screen.findAllByText('Issue 1') const calls = (api.get as any).mock.calls.filter((c: any[]) => !String(c[0]).startsWith('/metadata') && !String(c[0]).endsWith('/facets')) expect(calls.length).toBeGreaterThan(0) for (const c of calls) { expect(c[1].params.sortBy).toBe('title') expect(c[1].params.order).toBe('desc') } }) it('defaultSort without desc is asc; no defaultSort sends no sortBy', async () => { const a = fakeApi(5) const { unmount } = render( , ) await screen.findAllByText('Issue 1') const listCall = (a.get as any).mock.calls.find((c: any[]) => c[1]?.params?.page) expect(listCall[1].params.order).toBe('asc') unmount() const b = fakeApi(5) render( , ) await screen.findAllByText('Issue 1') const call = (b.get as any).mock.calls.find((c: any[]) => c[1]?.params?.page) expect(call[1].params.sortBy).toBeUndefined() }) it('a URL sortBy wins over defaultSort when enableUrlSync is on', async () => { window.history.replaceState({}, '', '/?sortBy=id&order=asc') const api = fakeApi(5) render( , ) await screen.findAllByText('Issue 1') const lists = (api.get as any).mock.calls.filter((c: any[]) => c[1]?.params?.page) for (const c of lists) { // never the default; (a pre-adoption request without sort is pre-existing behavior) expect(c[1].params.sortBy).not.toBe('title') } await waitFor(() => { const all = (api.get as any).mock.calls.filter((c: any[]) => c[1]?.params?.page) expect(all[all.length - 1][1].params).toMatchObject({ sortBy: 'id', order: 'asc' }) }) }) }) describe('DynamicTable bulk context actions / can', () => { beforeEach(() => { localStorage.clear(); sessionStorage.clear() useMetadataCache.getState().setMetadata('issue', meta()) }) function Probe({ seen }: { seen: { keys: string[]; approve: boolean; reject: boolean }[] }) { return ( { seen.push({ keys: ctx.actions.map((a) => a.key), approve: ctx.can('approve'), reject: ctx.can('reject') }) return Barra }} /> ) } it('without a provider exposes all actions and can() is true', async () => { const seen: any[] = [] render() await selectFirstRow() await screen.findByText('Barra') const last = seen[seen.length - 1] expect(last.keys).toEqual(['approve', 'reject']) expect(last.approve).toBe(true) expect(last.reject).toBe(true) }) it('with a provider filters actions and can() by capability', async () => { const seen: any[] = [] render( , ) await selectFirstRow() await screen.findByText('Barra') const last = seen[seen.length - 1] expect(last.keys).toEqual(['approve']) expect(last.approve).toBe(true) expect(last.reject).toBe(false) }) })