// @vitest-environment happy-dom // // DynamicTable classic pagination ('pages', the default mode): // 1. The pager footer renders; clicking page 2 issues a NEW query with // page=2 and REPLACES the visible rows (no accumulation). // 2. Changing the filters resets back to page 1. // 3. The chosen page size persists per table in localStorage and is adopted // on the next mount (over the model's server default). 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 { ApiProvider, type ApiClient } from '../api-context' import { useMetadataCache } from '../metadata-cache' import type { TableMetadata } from '../types' afterEach(cleanup) function meta(): TableMetadata { return { title: 'Issues', endpoint: '/data/issue', group_by: 'stage', 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 } } }) const page = (n: number, size: number) => { const start = (n - 1) * size const rows: Array<{ id: number; title: string }> = [] for (let i = start; i < Math.min(start + size, total); i++) { rows.push({ id: i + 1, title: `Issue ${i + 1}` }) } return rows } 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 return ok(page(p, size)) }), post: vi.fn(async () => ok(null)), put: vi.fn(async () => ok(null)), delete: vi.fn(async () => ok(null)), } } function dataCalls(api: ApiClient) { return (api.get as any).mock.calls.filter((c: any[]) => c[0] === '/data/issue') } describe('DynamicTable classic pages mode (default)', () => { beforeEach(() => { localStorage.clear() sessionStorage.clear() useMetadataCache.getState().setMetadata('issue', meta()) }) it('changes page: new query with page=2 and REPLACES the rows', async () => { const api = fakeApi(45) render( , ) // Page 1 loads with the model's default size. await waitFor(() => { const first = dataCalls(api)[0] expect(first?.[1]?.params).toMatchObject({ page: 1, per_page: 10 }) }) await waitFor(() => expect(screen.getAllByText('Issue 1').length).toBeGreaterThan(0)) // Click the pager's page "2" button. const btn2 = screen.getAllByRole('button').find((b) => b.textContent?.trim().endsWith('2')) expect(btn2).toBeTruthy() fireEvent.click(btn2!) await waitFor(() => { const pages = dataCalls(api).map((c: any[]) => c[1]?.params?.page) expect(pages).toContain(2) }) // Rows were REPLACED, not accumulated: page-2 rows in, page-1 rows out. await waitFor(() => expect(screen.getAllByText('Issue 11').length).toBeGreaterThan(0)) expect(screen.queryByText('Issue 1')).toBeNull() }) it('resets to page 1 when the filters change', async () => { const api = fakeApi(45) const { rerender } = render( , ) await waitFor(() => expect(screen.getAllByText('Issue 1').length).toBeGreaterThan(0)) const btn2 = screen.getAllByRole('button').find((b) => b.textContent?.trim().endsWith('2')) fireEvent.click(btn2!) await waitFor(() => expect(screen.getAllByText('Issue 11').length).toBeGreaterThan(0)) // Change the active filters via defaultFilters (part of buildFilterParams). rerender( , ) // The filtered request goes back to page 1. await waitFor(() => { const withFilter = dataCalls(api).filter((c: any[]) => c[1]?.params?.f_stage === 'done') expect(withFilter.length).toBeGreaterThan(0) expect(withFilter.every((c: any[]) => c[1]?.params?.page === 1)).toBe(true) }) await waitFor(() => expect(screen.getAllByText('Issue 1').length).toBeGreaterThan(0)) }) it('adopts the per-table page size persisted in localStorage', async () => { localStorage.setItem('mc:tbl:pageSize:v1|issue', '20') const api = fakeApi(45) render( , ) await waitFor(() => { const first = dataCalls(api)[0] expect(first?.[1]?.params).toMatchObject({ page: 1, per_page: 20 }) }) }) })