diff --git a/panel/src/pages/servers/ServersPage.test.tsx b/panel/src/pages/servers/ServersPage.test.tsx index 33f4a62..51c1eba 100644 --- a/panel/src/pages/servers/ServersPage.test.tsx +++ b/panel/src/pages/servers/ServersPage.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest"; -import { render, screen, within } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach, beforeAll, afterEach } from "vitest"; +import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import type { FleetServer, MyServerView } from "@/lib/types"; @@ -341,3 +341,60 @@ describe("ServersPage order and paging", () => { expect(await tableOrder()).toEqual(names.slice(0, 20)); }); }); + +describe("ServersPage with nothing matching", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("clears the search and the phase filter from the empty state", async () => { + tier.isAdmin = false; + const mine = (name: string): MyServerView => ({ + name, + subdomain: name, + owned: true, + claimable: false, + phase: "Running", + playersOnline: 0, + playersMax: 20, + }); + calls.myServers.mockResolvedValue([mine("survival"), mine("creative")]); + const user = renderPage(); + await tableOrder(); + + const search = screen.getByPlaceholderText(/Search/) as HTMLInputElement; + await user.type(search, "zzz"); + await user.click(screen.getByRole("combobox", { name: "Filter by phase" })); + await user.click(await screen.findByRole("option", { name: "Stopping" })); + expect(screen.getByText("No matching servers found.")).toBeTruthy(); + expect(screen.getByText("No server matches the current search or filter.")).toBeTruthy(); + + await user.click(screen.getByRole("button", { name: "Clear filters" })); + expect(await tableOrder()).toEqual(["creative", "survival"]); + expect(search.value).toBe(""); + expect(screen.getByRole("combobox", { name: "Filter by phase" }).textContent).toBe("All phases"); + }); + + it("starts the cleared list from its first page", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + const names = Array.from({ length: 21 }, (_, i) => `srv-${String(i + 1).padStart(2, "0")}`); + calls.fleet.mockResolvedValue(names.map((n) => row(n, { phase: "Running", ready: true }))); + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); + render( + + + , + ); + await tableOrder(); + await user.click(screen.getByRole("combobox", { name: "Filter by phase" })); + await user.click(await screen.findByRole("option", { name: "Running" })); + await user.click(screen.getByRole("button", { name: /Next/ })); + expect(await tableOrder()).toEqual(["srv-21"]); + + // The next reread finds every server stopped, so the filtered page empties. + calls.fleet.mockResolvedValue(names.map((n) => row(n, {}))); + await act(() => vi.advanceTimersByTimeAsync(10_000)); + await user.click(await screen.findByRole("button", { name: "Clear filters" })); + expect(await tableOrder()).toEqual(names.slice(0, 20)); + }); +}); diff --git a/panel/src/pages/servers/ServersPage.tsx b/panel/src/pages/servers/ServersPage.tsx index ff7e05b..fe990ae 100644 --- a/panel/src/pages/servers/ServersPage.tsx +++ b/panel/src/pages/servers/ServersPage.tsx @@ -402,8 +402,20 @@ export function ServersPage() { ) : visible.length === 0 ? ( + hint={t("fleet_no_match_hint")} + > + + ) : ( <> {/* Cards until the table fits (two per row from md). The admin