Files
fleet/frontend/components/TableContainer/TableContainer.tests.tsx
T
Rahul Raghunathan c92b848919 Return to previous page when the last policy on a page is deleted (#48683)
**Related issue:** Resolves #48641

## Description

Deleting the only policy on a paginated page (e.g., 21 policies, with 1
on page 2) left the user stranded on a now-empty page showing the "No
policies" empty state. The policies list now steps back to the previous
page when a delete empties the current page.

**Before:** delete last policy on page 2 → empty state.
**After:** delete last policy on page 2 → list returns to page 1.

### Screen recording demonstrating the fix


https://github.com/user-attachments/assets/ae106a50-7f9b-4080-a19c-53e0c60fff48


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

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

* **Bug Fixes**
* Server-side paginated tables now recover from empty states after
deleting the last row on a page by redirecting to the last page that
still has data.
* Improved empty-state pagination handling for out-of-range pages,
loading states, and cases where the total row count is known (including
zero), avoiding unnecessary or repeated navigation.
* Simplified the empty-state pagination UI to render only the empty
component.
* **Tests**
* Expanded regression test coverage for server-side pagination edge
cases and page-correction behavior to prevent future regressions.
* **Style**
* Removed unused empty/previous-button styling rules in the table
container.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-08 13:14:14 -03:00

175 lines
5.2 KiB
TypeScript

import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import TableContainer, { ITableQueryData } from "./TableContainer";
const COLUMN_CONFIGS = [
{
title: "Name",
Header: "Name",
accessor: "name",
disableSortBy: true,
},
];
const EmptyComponent = () => <div>No items found</div>;
// pageIndex requested by the most recent onQueryChange call.
const lastRequestedPageIndex = (onQueryChange: jest.Mock) => {
const { calls } = onQueryChange.mock;
return (calls[calls.length - 1][0] as ITableQueryData).pageIndex;
};
const PAGE_SIZE = 20;
// Simulates a real parent: pageIndex is URL-driven, and the data shown is
// derived from the requested page. Used to exercise the page-correction effect
// end-to-end (and guard against navigation feedback loops).
const ServerPaginatedTable = ({
initialPage,
totalCount,
}: {
initialPage: number;
totalCount: number;
}) => {
const [page, setPage] = useState(initialPage);
const start = page * PAGE_SIZE;
const rows = Array.from({
length: Math.max(0, Math.min(PAGE_SIZE, totalCount - start)),
}).map((_, i) => ({ name: `row ${start + i}` }));
return (
<TableContainer
columnConfigs={COLUMN_CONFIGS}
data={rows}
isLoading={false}
emptyComponent={EmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
pageIndex={page}
onQueryChange={(q: ITableQueryData) => setPage(q.pageIndex)}
defaultSortHeader="name"
/>
);
};
describe("TableContainer - server-side empty page", () => {
it("navigates back to the last page with data when a non-first page is empty", async () => {
const onQueryChange = jest.fn();
render(
<TableContainer
columnConfigs={COLUMN_CONFIGS}
data={[]}
isLoading={false}
emptyComponent={EmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
pageIndex={1}
totalCount={20}
onQueryChange={onQueryChange}
defaultSortHeader="name"
/>
);
// The empty page (index 1) is not a resting state: the table should request
// the last page that actually has data (index 0 here).
await waitFor(() => {
expect(onQueryChange).toHaveBeenCalled();
expect(lastRequestedPageIndex(onQueryChange)).toBe(0);
});
});
it("shows the empty state and stays put on the first page", async () => {
const onQueryChange = jest.fn();
render(
<TableContainer
columnConfigs={COLUMN_CONFIGS}
data={[]}
isLoading={false}
emptyComponent={EmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
pageIndex={0}
totalCount={0}
onQueryChange={onQueryChange}
defaultSortHeader="name"
/>
);
expect(await screen.findByText("No items found")).toBeInTheDocument();
await waitFor(() => {
expect(lastRequestedPageIndex(onQueryChange)).toBe(0);
});
});
it("does not redirect while the page is still loading", () => {
const onQueryChange = jest.fn();
render(
<TableContainer
columnConfigs={COLUMN_CONFIGS}
data={[]}
isLoading
emptyComponent={EmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
pageIndex={1}
totalCount={20}
onQueryChange={onQueryChange}
defaultSortHeader="name"
/>
);
// While loading we can't know the page is truly empty, so the requested
// page must never be corrected away from the one that was asked for.
const requestedPageIndexes = onQueryChange.mock.calls.map(
(call) => (call[0] as ITableQueryData).pageIndex
);
expect(requestedPageIndexes).not.toContain(0);
});
// Regression: entering an out-of-range page via the URL must settle on the
// last page with data without looping.
it("settles on the last page with data when entered on an out-of-range page", async () => {
// 21 rows -> pages 0 (20 rows) and 1 (1 row); pages >= 2 are empty.
render(<ServerPaginatedTable initialPage={3} totalCount={21} />);
await waitFor(() => {
expect(screen.getByText("row 20")).toBeInTheDocument();
});
expect(screen.queryByText("No items found")).not.toBeInTheDocument();
});
it("jumps straight to the first page when the total count is a known zero", async () => {
const onQueryChange = jest.fn();
render(
<TableContainer
columnConfigs={COLUMN_CONFIGS}
data={[]}
isLoading={false}
emptyComponent={EmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
pageIndex={3}
totalCount={0}
onQueryChange={onQueryChange}
defaultSortHeader="name"
/>
);
await waitFor(() => {
expect(onQueryChange).toHaveBeenCalled();
expect(lastRequestedPageIndex(onQueryChange)).toBe(0);
});
// A known-empty count should jump straight to page 0, not step 3 -> 2 -> 1.
const requestedPageIndexes = onQueryChange.mock.calls.map(
(call) => (call[0] as ITableQueryData).pageIndex
);
expect(requestedPageIndexes).not.toContain(2);
expect(requestedPageIndexes).not.toContain(1);
});
});