Repository: DouglasNeuroInformatics/libui
Pull request #49: Add Client table first and last page buttons
Author: login: david-roper
Labels: name: released
Created: 2025-03-25T20:36:45Z | Merged: 2025-03-26T17:32:56Z

closes issue #48 

Allows users to go to the first page or the last page of the client table data.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced "First Page" and "Last Page" navigation buttons on the client table, enabling quick jumps to the beginning or end of the dataset.
  - Added localized labels for these buttons in English and French for an improved user experience.
  
- **Tests**
  - Introduced a new test suite for the `ClientTable` component to ensure correct rendering and functionality.
  
- **Enhancements**
  - Added a data attribute for easier targeting of the `ClientTable` component in testing scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/components/ClientTable/ClientTable.spec.tsx b/src/components/ClientTable/ClientTable.spec.tsx
new file mode 100644
index 00000000..f2605aec
--- /dev/null
+++ b/src/components/ClientTable/ClientTable.spec.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { ClientTable } from './ClientTable';
+
+const TEST_ID = 'ClientTable';
+const FIRST_BUTTON_ID = 'first-page-button';
+const PREVIOUS_BUTTON_ID = 'previous-page-button';
+const NEXT_BUTTON_ID = 'next-page-button';
+const LAST_BUTTON_ID = 'last-page-button';
+
+describe('ClientTable', () => {
+  it('should render', () => {
+    render(<ClientTable columns={[]} data={[]} minRows={10} />);
+    expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
+    expect(screen.getByTestId(FIRST_BUTTON_ID)).toBeInTheDocument();
+    expect(screen.getByTestId(PREVIOUS_BUTTON_ID)).toBeInTheDocument();
+    expect(screen.getByTestId(NEXT_BUTTON_ID)).toBeInTheDocument();
+    expect(screen.getByTestId(LAST_BUTTON_ID)).toBeInTheDocument();
+  });
+  it('should contain a custom class name', () => {
+    render(<ClientTable className="foo" columns={[]} data={[]} minRows={10} />);
+    expect(screen.getByTestId(TEST_ID)).toHaveClass('foo');
+  });
+});
diff --git a/src/components/ClientTable/ClientTable.tsx b/src/components/ClientTable/ClientTable.tsx
index 256a34f5..83878e0f 100644
--- a/src/components/ClientTable/ClientTable.tsx
+++ b/src/components/ClientTable/ClientTable.tsx
@@ -85,7 +85,7 @@ export const ClientTable = <T extends ClientTableEntry>({
   const nRows = Math.max(currentEntries.length, minRows ?? -1);
 
   return (
-    <div className={className} {...props}>
+    <div className={className} {...props} data-testid="ClientTable">
       <div className="rounded-md border bg-card tracking-tight text-muted-foreground shadow-sm">
         <Table>
           <Table.Header>
diff --git a/src/components/ClientTable/ClientTablePagination.tsx b/src/components/ClientTable/ClientTablePagination.tsx
index 350c7d78..395c8323 100644
--- a/src/components/ClientTable/ClientTablePagination.tsx
+++ b/src/components/ClientTable/ClientTablePagination.tsx
@@ -27,6 +27,18 @@ export const ClientTablePagination = ({
       </div>
       <div className="flex flex-1 justify-between gap-3 sm:justify-end">
         <Button
+          data-testid="first-page-button"
+          disabled={currentPage === 1}
+          type="button"
+          variant="outline"
+          onClick={() => {
+            setCurrentPage(1);
+          }}
+        >
+          {t('pagination.firstPage')}
+        </Button>
+        <Button
+          data-testid="previous-page-button"
           disabled={currentPage === 1}
           type="button"
           variant="outline"
@@ -37,6 +49,7 @@ export const ClientTablePagination = ({
           {t('pagination.previous')}
         </Button>
         <Button
+          data-testid="next-page-button"
           disabled={currentPage === pageCount}
           type="button"
           variant="outline"
@@ -46,6 +59,17 @@ export const ClientTablePagination = ({
         >
           {t('pagination.next')}
         </Button>
+        <Button
+          data-testid="last-page-button"
+          disabled={currentPage === pageCount}
+          type="button"
+          variant="outline"
+          onClick={() => {
+            setCurrentPage(pageCount);
+          }}
+        >
+          {t('pagination.lastPage')}
+        </Button>
       </div>
     </div>
   );
diff --git a/src/i18n/translations/libui.json b/src/i18n/translations/libui.json
index 73583e6b..86047a31 100644
--- a/src/i18n/translations/libui.json
+++ b/src/i18n/translations/libui.json
@@ -143,6 +143,14 @@
     "previous": {
       "en": "Previous",
       "fr": "Précédent"
+    },
+    "firstPage": {
+      "en": "<< First",
+      "fr": "<< Première"
+    },
+    "lastPage": {
+      "en": "Last >>",
+      "fr": "Dernière >>"
     }
   },
   "searchBar": {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (4)</summary><blockquote>

`8-25`: **Consider simplifying the test data structure**

The `ExampleItem` type declaration includes 15 numeric fields which seems excessive for this test. Best practice is to keep test data minimal while still being representative of real use cases.


```diff
-type ExampleItem = {
-  c1: number;
-  c2: number;
-  c3: number;
-  c4: number;
-  c5: number;
-  c6: number;
-  c7: number;
-  c8: number;
-  c9: number;
-  c10: number;
-  c11: number;
-  c12: number;
-  c13: number;
-  c14: number;
-  c15: number;
-  id: string;
-};
+type ExampleItem = {
+  id: string;
+  value1: number;
+  value2: number;
+  value3: number;
+};
```

---

`49-53`: **Improve test callback implementation**

The current implementation of `onSelection` just returns the column label but doesn't test any actual behavior. Consider using a mock function to verify it's being called correctly.


```diff
-            onSelection: (column) => {
-              return column.label;
-            }
+            onSelection: vi.fn()
```

---

`78-80`: **Improve test callback implementation**

Similar to the `onSelection` callback, the `onEntryClick` just returns the entry. Consider using a mock function to verify it's being called properly.


```diff
-        onEntryClick={(entry) => {
-          return entry;
-        }}
+        onEntryClick={vi.fn()}
```

---

`69-75`: **Consider test data consistency**

The test data uses fields `f1`, `f2`, and `f3` which don't match the `ExampleItem` type defined earlier. This inconsistency can lead to confusion.


Make the test data consistent with your type definitions or adjust your types to match the actual test data:

```diff
        data={[
          {
-            f1: 1,
-            f2: range(1000).join(', '),
-            f3: 3
+            id: '1',
+            value1: 1,
+            value2: 2, 
+            value3: 3
          }
        ]}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3b24cecb9fedab7919cd03a2630750449eec1300 and f8cfa434662407984c81b0c523c9dbb7afa01f4c.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/components/ClientTable/ClientTable.spec.tsx` (1 hunks)
* `src/components/ClientTable/ClientTable.tsx` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.tsx (1)</summary>

`88-88`: **LGTM: Test ID added for component testing**

Adding the `data-testid` attribute is a good practice for testing React components, as it provides a reliable way to target the component in tests.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T15:56:56Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (4)</summary><blockquote>

`38-38`: **Simplify test data to improve test readability and performance.**

Using `range(1000).join(', ')` creates an unnecessarily large string which doesn't add testing value and could slow down test execution.


```diff
-            f2: range(1000).join(', '),
+            f2: 'Sample text content',
```

---

`44-46`: **Use mock functions for callbacks to improve test effectiveness.**

The current callback simply returns the parameter, which doesn't provide meaningful test coverage.


```diff
-        onEntryClick={(entry) => {
-          return entry;
-        }}
+        onEntryClick={vi.fn()}
```

This would allow you to verify the callback is called with the expected arguments in future tests.

---

`16-18`: **Use mock functions for dropdown callbacks.**

Similar to the `onEntryClick` handler, this callback doesn't provide meaningful test coverage.


```diff
-            onSelection: (column) => {
-              return column.label;
-            }
+            onSelection: vi.fn()
```

---

`1-7`: **Consider adding a descriptive test file comment.**

Adding a brief comment at the top of the file explaining the purpose of these tests would improve documentation.


```diff
+/**
+ * Tests for the ClientTable component
+ * Verifies basic rendering and pagination functionality
+ */
 import { range } from '@douglasneuroinformatics/libjs';
 import { render, screen } from '@testing-library/react';
 import { describe, expect, it } from 'vitest';
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f8cfa434662407984c81b0c523c9dbb7afa01f4c and 8619d1c6dcfbdf10dd49114d65837533d2d11840.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/components/ClientTable/ClientTable.spec.tsx` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (1)</summary>

`9-50`: **Add tests for pagination and new first/last page functionality.**

This test only verifies that the component renders, but doesn't test the pagination functionality or the new first/last page buttons that this PR is intended to add.
 

Consider adding tests that specifically:
1. Verify first/last page buttons are present
2. Check that buttons are correctly disabled/enabled based on current page
3. Confirm clicking these buttons triggers the expected page change

```javascript
// Example test structure to add:
it('should handle pagination navigation correctly', async () => {
  const onPageChange = vi.fn();
  render(
    <ClientTable
      // ...existing props
      data={Array(30).fill(0).map((_, i) => ({ f1: i, f2: `Row ${i}`, f3: i * 10 }))}
      pageSize={10}
      pagination={true}
      onPageChange={onPageChange}
    />
  );
  
  // Get pagination buttons
  const firstPageButton = screen.getByRole('button', { name: /first page/i });
  const lastPageButton = screen.getByRole('button', { name: /last page/i });
  
  // Test disabled state on first page
  expect(firstPageButton).toBeDisabled();
  
  // Navigate to second page
  await userEvent.click(screen.getByRole('button', { name: /next/i }));
  expect(onPageChange).toHaveBeenCalledWith(expect.objectContaining({ pageIndex: 1 }));
  
  // Test first page button is now enabled
  expect(firstPageButton).not.toBeDisabled();
  
  // More navigation tests...
});
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T17:06:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (1)</summary><blockquote>

`12-20`: _🛠️ Refactor suggestion_

**Enhance tests for pagination functionality**

While the basic rendering tests are a good start, they don't verify the actual functionality of the pagination buttons (especially the new first/last page buttons that are the focus of this PR). The tests only check if elements are present in the document, not if they work correctly.



Consider adding these tests:
1. Verify buttons are disabled in appropriate states (e.g., first/previous buttons disabled on first page)
2. Verify page navigation works by clicking buttons and checking if callbacks are called with correct page numbers
3. Test page data updates correctly after navigation

Example test enhancement:

```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

// Add more comprehensive tests
it('should disable first/previous buttons on first page', () => {
  render(<ClientTable columns={[]} data={[]} minRows={10} />);
  expect(screen.getByTestId(FIRST_BUTTON_ID)).toBeDisabled();
  expect(screen.getByTestId(PREVIOUS_BUTTON_ID)).toBeDisabled();
});

it('should call onPageChange when pagination buttons are clicked', () => {
  const onPageChange = vi.fn();
  // Render with enough data to have multiple pages
  render(<ClientTable 
    columns={[{field: 'id', label: 'ID'}]} 
    data={Array(50).fill(0).map((_, i) => ({id: i}))} 
    pageSize={10}
    onPageChange={onPageChange}
  />);
  
  // Click next page button
  fireEvent.click(screen.getByTestId(NEXT_BUTTON_ID));
  expect(onPageChange).toHaveBeenCalledWith(expect.objectContaining({ pageIndex: 1 }));
  
  // Click last page button
  fireEvent.click(screen.getByTestId(LAST_BUTTON_ID));
  expect(onPageChange).toHaveBeenCalledWith(expect.objectContaining({ pageIndex: 4 }));
});
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (2)</summary><blockquote>

`7-10`: **Improve test ID naming for consistency**

The test ID constants are using different naming patterns - some with hyphens and some with underscores. Consistent naming improves code maintainability.


Consider standardizing on one pattern:

```diff
-const TEST_ID = 'ClientTable';
-const FIRST_BUTTON_ID = 'first-page-button';
-const PREVIOUS_BUTTON_ID = 'previous-page-button';
-const NEXT_BUTTON_ID = 'next-page-button';
-const LAST_BUTTON_ID = 'last-page-button';
+const TEST_ID = 'client-table';
+const FIRST_BUTTON_ID = 'first-page-button';
+const PREVIOUS_BUTTON_ID = 'previous-page-button';
+const NEXT_BUTTON_ID = 'next-page-button';
+const LAST_BUTTON_ID = 'last-page-button';
```

Or:

```diff
-const TEST_ID = 'ClientTable';
-const FIRST_BUTTON_ID = 'first-page-button';
-const PREVIOUS_BUTTON_ID = 'previous-page-button';
-const NEXT_BUTTON_ID = 'next-page-button';
-const LAST_BUTTON_ID = 'last-page-button';
+const TEST_ID = 'client_table';
+const FIRST_BUTTON_ID = 'first_page_button';
+const PREVIOUS_BUTTON_ID = 'previous_page_button';
+const NEXT_BUTTON_ID = 'next_page_button';
+const LAST_BUTTON_ID = 'last_page_button';
```

---

`21-24`: **Enhance class name test with additional assertions**

The test for custom class names could be more robust by verifying that both the custom class and default classes are present.


Consider expanding the test:

```typescript
it('should apply custom class name while preserving default classes', () => {
  render(<ClientTable className="foo" columns={[]} data={[]} minRows={10} />);
  const table = screen.getByTestId(TEST_ID);
  
  // Verify custom class is applied
  expect(table).toHaveClass('foo');
  
  // Verify default classes are still present (update with actual default classes)
  expect(table).toHaveClass('default-class-1');
  expect(table).toHaveClass('default-class-2');
});
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2055ff358c2e52c27537a92fedc5d656dcc17f8a and 31e6630eaf46006a0e9b8b59292069fb12324110.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/components/ClientTable/ClientTable.spec.tsx` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/components/ClientTable/ClientTable.spec.tsx (1)</summary>

`1-5`: **LGTM! Imports are well-organized**

The imports are properly organized with testing library imports first, followed by a blank line, then the component import.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T17:23:26Z

Review comments:
diff_hunk: @@ -0,0 +1,85 @@
+import { range } from '@douglasneuroinformatics/libjs';
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { ClientTable } from './ClientTable';
+import { type ClientTableColumn } from './ClientTable';
+
+type ExampleItem = {
+  c1: number;
+  c2: number;
+  c3: number;
+  c4: number;
+  c5: number;
+  c6: number;
+  c7: number;
+  c8: number;
+  c9: number;
+  c10: number;
+  c11: number;
+  c12: number;
+  c13: number;
+  c14: number;
+  c15: number;
+  id: string;
+};
+
+const columns: ClientTableColumn<ExampleItem>[] = [
+  {
+    field: 'id',
+    label: 'ID'
+  }
+];
+
+for (let i = 1; i < 16; i++) {
+  columns.push({
+    field: `c${i}` as keyof ExampleItem,
+    label: `Column ${i}`
+  });
+}
path: src/components/ClientTable/ClientTable.spec.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**The defined columns aren't used in the test**

You've defined a set of columns here, but the actual test uses a different set of column definitions (using f1, f2, f3 fields). This creates confusion and unnecessary code.


Either use these columns in the test or remove this unused definition:

```diff
-const columns: ClientTableColumn<ExampleItem>[] = [
-  {
-    field: 'id',
-    label: 'ID'
-  }
-];
-
-for (let i = 1; i < 16; i++) {
-  columns.push({
-    field: `c${i}` as keyof ExampleItem,
-    label: `Column ${i}`
-  });
-}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion

`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 8619d1c
created_at: 2025-03-26T15:56:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,85 @@
+import { range } from '@douglasneuroinformatics/libjs';
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { ClientTable } from './ClientTable';
+import { type ClientTableColumn } from './ClientTable';
+
+type ExampleItem = {
+  c1: number;
+  c2: number;
+  c3: number;
+  c4: number;
+  c5: number;
+  c6: number;
+  c7: number;
+  c8: number;
+  c9: number;
+  c10: number;
+  c11: number;
+  c12: number;
+  c13: number;
+  c14: number;
+  c15: number;
+  id: string;
+};
+
+const columns: ClientTableColumn<ExampleItem>[] = [
+  {
+    field: 'id',
+    label: 'ID'
+  }
+];
+
+for (let i = 1; i < 16; i++) {
+  columns.push({
+    field: `c${i}` as keyof ExampleItem,
+    label: `Column ${i}`
+  });
+}
+
+const TEST_ID = 'ClientTable';
+
+describe('ClientTable', () => {
+  it('should render', () => {
+    render(
+      <ClientTable
+        columnDropdownOptions={[
+          {
+            label: 'Delete',
+            onSelection: (column) => {
+              return column.label;
+            }
+          }
+        ]}
+        columns={[
+          {
+            field: 'f1',
+            label: 'Field 1'
+          },
+          {
+            field: 'f2',
+            label: 'Field 2'
+          },
+          {
+            field: 'f3',
+            label: 'Field 3'
+          }
+        ]}
+        data={[
+          {
+            f1: 1,
+            f2: range(1000).join(', '),
+            f3: 3
+          }
+        ]}
+        minRows={10}
+        noWrap={true}
+        onEntryClick={(entry) => {
+          return entry;
+        }}
+      />
+    );
+    expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
+  });
path: src/components/ClientTable/ClientTable.spec.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

_🛠️ Refactor suggestion_

<details>
<summary>🧩 Analysis chain</summary>

**Add tests for pagination and new first/last page functionality**

This test only verifies that the component renders, but doesn't test the pagination functionality or the new first/last page buttons that this PR is intended to add.



Consider adding a test that specifically verifies the pagination functionality works as expected, particularly the new first and last page buttons. This would ensure the PR objectives are properly tested.

---

🌐 Web query:

```web
How to test pagination with React Testing Library?
```

💡 Result:

To test pagination components effectively with React Testing Library, follow these core strategies:

---

### **Rendering and Element Selection**
1. **Render component with required props**  
   Mock pagination handlers and provide initial state:
   ```jsx
   const onChangePage = vi.fn();
   render(
     <Pagination
       currentPage={1}
       totalItems={50}
       itemsPerPage={10}
       onPageChange={onChangePage}
     />
   );
   ```

2. **Locate elements using roles and text**  
   Use `getByRole`, `getByText`, or `within` for complex sections:
   ```javascript
   const prevButton = screen.getByRole('button', { name: /previous/i });
   const page2Button = screen.getByRole('button', { name: '2' });
   const paginationInfo = screen.getByTestId('pagination-info');
   ```

---

### **Interaction Testing**
1. **Simulate user navigation**  
   Use `userEvent` to click buttons and validate state changes:
   ```javascript
   await userEvent.click(page2Button);
   expect(onChangePage).toHaveBeenCalledWith(2);
   ```

2. **Test edge cases**  
   Verify buttons are disabled on first/last page:
   ```javascript
   // On first page
   expect(prevButton).toBeDisabled();

   // After navigating to page 5
   await userEvent.click(screen.getByRole('button', { name: '5' }));
   expect(screen.getByRole('button', { name: /next/i })).toBeDisabled();
   ```

---

### **Async Updates and State Validation**
1. **Wait for UI updates**  
   Use `waitFor` to handle asynchronous re-renders:
   ```javascript
   test('updates displayed page after click', async () => {
     await userEvent.click(page2Button);
     await waitFor(() => {
       expect(paginationInfo).toHaveTextContent('Page 2 of 5');
     });
   });
   ```

2. **Validate rendered content**  
   Check if the correct data slice is displayed after pagination:
   ```javascript
   const items = screen.getAllByRole('listitem');
   expect(items).toHaveLength(10); // Items per page
   ```

---

### **Advanced Scenarios**
1. **Dynamic page size**  
   Test dropdowns for items-per-page selection:
   ```javascript
   const pageSizeSelect = screen.getByLabelText('Items per page');
   await userEvent.selectOptions(pageSizeSelect, '20');
   expect(onPageSizeChange).toHaveBeenCalledWith(20);
   ```

2. **API-driven pagination**  
   Mock API responses and validate loading states (using TanStack Query example):
   ```javascript
   jest.mock('@tanstack/react-query', () => ({
     useQuery: () => ({
       data: mockPageData,
       isLoading: false,
     }),
   }));
   ```

---

### **Recommended Assertions**
- Verify button states (enabled/disabled)
- Validate displayed page numbers
- Check if `onPageChange` is called with correct values
- Confirm rendered data matches the expected page

For complex components like tables with integrated pagination, use utilities like `within` to scope queries[3][6]:
  ```javascript
  const paginationSection = screen.getByRole('navigation', { name: 'pagination' });
  const { getAllByRole } = within(paginationSection);
  expect(getAllByRole('button')).toHaveLength(5);
  ```

---

**Key Tools**  
- `userEvent` for realistic interactions  
- `waitFor` to handle async updates  
- `vi.fn()` (or `jest.fn()`) for mock callbacks  
- `within` for scoped element queries  

[1][3][4][6]

Citations:

- 1: https://serdarsen.hashnode.dev/how-did-i-test-my-pagination-component
- 2: https://developer.android.com/topic/libraries/architecture/paging/test
- 3: https://ncoughlin.com/posts/react-testing-library-usage
- 4: https://www.youtube.com/watch?v=bNLpK9sacyE
- 5: https://www.youtube.com/watch?v=jcKin2Hd900
- 6: https://blog.ag-grid.com/using-react-testing-library-with-ag-grid/
- 7: https://github.com/carbon-design-system/carbon/issues/12022
- 8: https://github.com/wojtekmaj/react-pdf/issues/564
- 9: https://codesandbox.io/s/react-table-pagination-test-78d41

---



</details>

**Enhance Testing for Pagination Functionality**

The current test only verifies that the component renders, but it doesn't cover the new pagination features introduced in this PR. Please add tests that validate the following:

- **Pagination Navigation:**  
  Simulate clicks on the new first/last page buttons (and other page controls) using `userEvent`. Ensure that clicking these buttons triggers the `onPageChange` (or similar) callback with the correct page numbers.

- **Button State Verification:**  
  Verify that the pagination buttons are appropriately enabled/disabled. For example, ensure that the "Previous" or "First" button is disabled on the first page and the "Next" or "Last" button is disabled on the last page.

- **UI Update Assertions:**  
  Use `waitFor` or similar async utilities to assert that the UI correctly updates to reflect the changed page number or data subset, such as a page indicator (e.g., "Page 2 of X") or expected data slice rendered after navigating.

Refer to the [guidelines on testing pagination with React Testing Library](https://serdarsen.hashnode.dev/how-did-i-test-my-pagination-component) for sample code and best practices.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-26T15:56:56Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-25T20:36:52Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This update introduces two navigation buttons to the `ClientTablePagination` component, allowing users to quickly jump to the first and last pages. The buttons are conditionally disabled based on the current page status. Additionally, new localization entries for these buttons have been added to the translation file to support English and French labels. A new test file for the `ClientTable` component has also been created to ensure its proper rendering. Furthermore, a `data-testid` attribute has been added to the `ClientTable` for improved test targeting.

## Changes

| File                                       | Change Summary                                                                                                          |
|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| `src/components/.../ClientTablePagination.tsx` | Added "First Page" and "Last Page" buttons with conditional disabling based on the current page position.              |
| `src/i18n/.../libui.json`                   | Introduced new translation entries for "firstPage" and "lastPage" with English ("<< First", "Last >>") and French ("<< Première", "Dernière >>") texts. |
| `src/components/.../ClientTable.spec.tsx` | Added a new test file for the `ClientTable` component to verify correct rendering and functionality.                   |
| `src/components/.../ClientTable.tsx`       | Added `data-testid="ClientTable"` attribute to the wrapping `<div>` for easier targeting in tests.                     |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant PT as ClientTablePagination

    U->>PT: Click "First Page" button
    alt Not on first page
        PT->>PT: Set currentPage to 1
        PT-->>U: Refresh table view
    else Already at first page
        PT-->>U: No action (button disabled)
    end

    U->>PT: Click "Last Page" button
    alt Not on last page
        PT->>PT: Set currentPage to pageCount
        PT-->>U: Refresh table view
    else Already at last page
        PT-->>U: No action (button disabled)
    end
```

## Poem
> I'm a rabbit with a digital hop,  
> Skipping pages until I reach the top.  
> First and last, now in my sight,  
> Translation magic makes it right.  
> With joyful hops in code so neat,  
> I celebrate these changes with a beat!  
> 🐰🎉

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=DouglasNeuroInformatics/libui&utm_content=49):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T20:12:25Z
body: :tada: This PR is included in version 3.9.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@douglasneuroinformatics/libui/v/3.9.0)
- [GitHub release](https://github.com/DouglasNeuroInformatics/libui/releases/tag/v3.9.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE